diff --git a/.github/workflows/search.yml b/.github/workflows/search.yml index 9d5aa59959a..7baa26f4981 100644 --- a/.github/workflows/search.yml +++ b/.github/workflows/search.yml @@ -45,6 +45,7 @@ jobs: sparse-checkout: | apps/docs apps/www/.env.local.example + examples supabase - uses: pnpm/action-setup@v4 @@ -63,7 +64,9 @@ jobs: - name: Update embeddings working-directory: ./apps/docs if: ${{ !inputs.refresh }} - run: pnpm run embeddings + run: | + pnpm run codegen:examples + pnpm run embeddings - name: Refresh embeddings working-directory: ./apps/docs diff --git a/apps/cms/.env b/apps/cms/.env new file mode 100644 index 00000000000..94f021dce47 --- /dev/null +++ b/apps/cms/.env @@ -0,0 +1,17 @@ +# [LOCAL ENV] - NEVER USE PROD VALUES LOCALLY +# as editing PayloadCMS typescript config INSTANTLY CHANGES DB SCHEMA in dev mode +# The values set here are the default values provided when running `supabase start`. +# Do not add secrets here, this file is git-tracked. Use .env.local instead. +DATABASE_URI=postgresql://postgres:postgres@127.0.0.1:34322/postgres +PAYLOAD_SECRET=secret + +S3_BUCKET=cms +S3_ACCESS_KEY_ID=625729a08b95bf1b7ff351a663f3a23c +S3_SECRET_ACCESS_KEY=850181e4652dd023b7a98c58ae0d2d34bd487ee0cc3254aed6eda37307425907 +S3_REGION=local +S3_ENDPOINT=http://127.0.0.1:34321/storage/v1/s3 + +BLOG_APP_URL=http://localhost:3000 +NEXT_PUBLIC_SERVER_URL=http://localhost:3000/blog +CRON_SECRET=secret +PREVIEW_SECRET=secret \ No newline at end of file diff --git a/apps/cms/.env.example b/apps/cms/.env.example deleted file mode 100644 index b8b497ca476..00000000000 --- a/apps/cms/.env.example +++ /dev/null @@ -1,13 +0,0 @@ -DATABASE_URI=postgres://postgres:@127.0.0.1:5432/your-database-name -PAYLOAD_SECRET=YOUR_SECRET_HERE - -S3_BUCKET= -S3_ACCESS_KEY_ID= -S3_SECRET_ACCESS_KEY= -S3_REGION= -S3_ENDPOINT= - -NEXT_PUBLIC_SERVER_URL=http://localhost:3000/blog -CRON_SECRET=YOUR_CRON_SECRET_HERE -PREVIEW_SECRET=YOUR_SECRET_HERE -BLOG_APP_URL=http://localhost:3000 \ No newline at end of file diff --git a/apps/cms/.eslintrc.js b/apps/cms/.eslintrc.cjs similarity index 100% rename from apps/cms/.eslintrc.js rename to apps/cms/.eslintrc.cjs diff --git a/apps/cms/.gitignore b/apps/cms/.gitignore index 3d53a40bcb8..954563c7fcb 100644 --- a/apps/cms/.gitignore +++ b/apps/cms/.gitignore @@ -29,7 +29,8 @@ yarn-debug.log* yarn-error.log* # local env files -.env*.local +!.env +.env.local # vercel .vercel @@ -38,6 +39,4 @@ yarn-error.log* *.tsbuildinfo next-env.d.ts -.env - /media diff --git a/apps/cms/.vercelignore b/apps/cms/.vercelignore new file mode 100644 index 00000000000..3b3aaed4c81 --- /dev/null +++ b/apps/cms/.vercelignore @@ -0,0 +1,48 @@ +# Dependencies +node_modules +**/node_modules + +# Build artifacts +.next/cache +.turbo +.eslintcache + +# Development files +*.log +.env.local +.env.*.local + +# Test files +**/*.test.* +**/*.spec.* +__tests__ +**/__tests__ +coverage + +# Documentation +README.md +*.md +docs + +# IDE files +.vscode +.idea +*.swp +*.swo + +# OS files +.DS_Store +Thumbs.db + +# Other apps (since we're only deploying CMS) +../studio +../www +../docs +../design-system +../ui-library +../../examples +../../tests +../../supabase +../../packages/ui-patterns +../../packages/ui +../../packages/shared-data \ No newline at end of file diff --git a/apps/cms/docker-compose.yml b/apps/cms/docker-compose.yml index dc5ddababa2..a475be621b0 100644 --- a/apps/cms/docker-compose.yml +++ b/apps/cms/docker-compose.yml @@ -15,7 +15,7 @@ services: env_file: - .env - # Uncomment the following to use postgres + # Uncomment the following to use postgres postgres: restart: always image: postgres:latest diff --git a/apps/cms/next.config.mjs b/apps/cms/next.config.mjs index cc7ee7f02ba..8c87e87b2f0 100644 --- a/apps/cms/next.config.mjs +++ b/apps/cms/next.config.mjs @@ -1,6 +1,23 @@ import { withPayload } from '@payloadcms/next/withPayload' -import redirects from './redirects.js' +const redirects = async () => { + const internetExplorerRedirect = { + destination: '/ie-incompatible.html', + has: [ + { + type: 'header', + key: 'user-agent', + value: '(.*Trident.*)', // all ie browsers + }, + ], + permanent: false, + source: '/:path((?!ie-incompatible.html$).*)', // all pages except the incompatibility page + } + + const redirects = [internetExplorerRedirect] + + return redirects +} const NEXT_PUBLIC_SERVER_URL = process.env.VERCEL_PROJECT_PRODUCTION_URL ? `https://${process.env.VERCEL_PROJECT_PRODUCTION_URL}` diff --git a/apps/cms/package.json b/apps/cms/package.json index 56250c8af63..ea0d762bd1a 100644 --- a/apps/cms/package.json +++ b/apps/cms/package.json @@ -5,43 +5,59 @@ "license": "MIT", "scripts": { "build": "cross-env NODE_OPTIONS=--no-deprecation next build --turbopack", + "ci": "pnpm migrate && pnpm build", + "clean": "rimraf node_modules .next", "dev": "cross-env NODE_OPTIONS=--no-deprecation next dev --turbopack --port 3030", + "dev:prod": "cross-env NODE_OPTIONS=--no-deprecation rm -rf .next && pnpm build && pnpm start", "devsafe": "rm -rf .next && cross-env NODE_OPTIONS=--no-deprecation next dev", "generate:importmap": "cross-env NODE_OPTIONS=--no-deprecation payload generate:importmap", "generate:types": "cross-env NODE_OPTIONS=--no-deprecation payload generate:types", "lint": "cross-env NODE_OPTIONS=--no-deprecation next lint", + "migrate": "cross-env NODE_OPTIONS=--no-deprecation tsx scripts/migrate.ts", "payload": "cross-env NODE_OPTIONS=--no-deprecation payload", - "start": "cross-env NODE_OPTIONS=--no-deprecation next start", - "ci": "cross-env NODE_OPTIONS=--no-deprecation payload migrate && pnpm build", - "clean": "rimraf node_modules .next", + "start": "cross-env NODE_OPTIONS=--no-deprecation next start --port 3030", + "vercel-build": "pnpm migrate && pnpm build", "typecheck_IGNORED": "tsc --noEmit" }, "dependencies": { - "@payloadcms/db-postgres": "3.33.0", - "@payloadcms/live-preview-react": "^3.33.0", - "@payloadcms/next": "3.33.0", - "@payloadcms/payload-cloud": "3.33.0", - "@payloadcms/plugin-form-builder": "3.33.0", - "@payloadcms/plugin-nested-docs": "3.33.0", - "@payloadcms/plugin-seo": "3.33.0", - "@payloadcms/richtext-lexical": "3.33.0", - "@payloadcms/storage-s3": "3.33.0", - "@payloadcms/ui": "3.33.0", + "@payloadcms/admin-bar": "^3.50.0", + "@payloadcms/db-postgres": "^3.50.0", + "@payloadcms/live-preview-react": "^3.50.0", + "@payloadcms/next": "3.50.0", + "@payloadcms/payload-cloud": "3.50.0", + "@payloadcms/plugin-form-builder": "3.50.0", + "@payloadcms/plugin-nested-docs": "3.50.0", + "@payloadcms/plugin-seo": "3.50.0", + "@payloadcms/richtext-lexical": "3.50.0", + "@payloadcms/storage-s3": "3.50.0", + "@payloadcms/ui": "3.50.0", + "@radix-ui/react-checkbox": "^1.3.2", + "@radix-ui/react-label": "^2.1.7", + "@radix-ui/react-select": "^2.0.0", + "@radix-ui/react-slot": "^1.2.3", + "class-variance-authority": "^0.7.1", + "clsx": "^1.2.1", "common": "workspace:*", "config": "workspace:*", "cross-env": "^7.0.3", "eslint-config-supabase": "workspace:*", - "graphql": "^16.8.1", + "graphql": "^16.11.0", + "image-size": "2.0.2", + "lucide-react": "^0.511.0", "next": "catalog:", - "payload": "3.33.0", + "payload": "3.50.0", + "pg": "^8.16.3", + "prism-react-renderer": "^2.3.1", "react": "catalog:", "react-dom": "catalog:", - "sharp": "0.32.6" + "sharp": "0.32.6", + "tailwind-merge": "^1.13.2" }, "devDependencies": { "@types/node": "catalog:", "@types/react": "catalog:", "@types/react-dom": "catalog:", + "tsx": "^4.19.3", "typescript": "5.7.3" } } diff --git a/apps/cms/redirects.js b/apps/cms/redirects.js deleted file mode 100644 index 21b76ecc1b8..00000000000 --- a/apps/cms/redirects.js +++ /dev/null @@ -1,20 +0,0 @@ -const redirects = async () => { - const internetExplorerRedirect = { - destination: '/ie-incompatible.html', - has: [ - { - type: 'header', - key: 'user-agent', - value: '(.*Trident.*)', // all ie browsers - }, - ], - permanent: false, - source: '/:path((?!ie-incompatible.html$).*)', // all pages except the incompatibility page - } - - const redirects = [internetExplorerRedirect] - - return redirects -} - -export default redirects diff --git a/apps/cms/scripts/migrate.ts b/apps/cms/scripts/migrate.ts new file mode 100644 index 00000000000..5d3a12c11e7 --- /dev/null +++ b/apps/cms/scripts/migrate.ts @@ -0,0 +1,28 @@ +import payload from 'payload' +// Use a minimal config for migrations to avoid importing the full app graph during build +import payloadConfig from '../src/payload.migrate.config.ts' + +async function run() { + try { + await payload.init({ config: payloadConfig }) + + // Ensure non-interactive: remove any dev-mode migration sentinel rows + try { + await payload.delete({ + collection: 'payload-migrations', + where: { batch: { equals: -1 } }, + }) + } catch {} + + await payload.db.migrate() + // eslint-disable-next-line no-console + console.log('✅ Payload migrations complete') + process.exit(0) + } catch (err) { + // eslint-disable-next-line no-console + console.error('❌ Payload migrations failed:', err) + process.exit(1) + } +} + +run() diff --git a/apps/cms/src/access/isAdmin.ts b/apps/cms/src/access/isAdmin.ts index 67922028caa..5e1261e26fb 100644 --- a/apps/cms/src/access/isAdmin.ts +++ b/apps/cms/src/access/isAdmin.ts @@ -1,5 +1,5 @@ import type { AccessArgs, FieldAccess } from 'payload' -import type { User } from '@/payload-types' +import type { User } from '../payload-types' type isAdmin = (args: AccessArgs) => boolean diff --git a/apps/cms/src/access/isAuthenticated.ts b/apps/cms/src/access/isAuthenticated.ts index 4112452bc3d..62d467a63e9 100644 --- a/apps/cms/src/access/isAuthenticated.ts +++ b/apps/cms/src/access/isAuthenticated.ts @@ -1,6 +1,6 @@ import type { AccessArgs } from 'payload' -import type { User } from '@/payload-types' +import type { User } from '../payload-types' type isAuthenticated = (args: AccessArgs) => boolean diff --git a/apps/cms/src/blocks/ArchiveBlock/Component.tsx b/apps/cms/src/blocks/ArchiveBlock/Component.tsx deleted file mode 100644 index 48a57ec5d69..00000000000 --- a/apps/cms/src/blocks/ArchiveBlock/Component.tsx +++ /dev/null @@ -1,65 +0,0 @@ -import type { Post, ArchiveBlock as ArchiveBlockProps } from '@/payload-types' - -import configPromise from '@payload-config' -import { getPayload } from 'payload' -import React from 'react' -import RichText from '@/components/RichText' - -import { CollectionArchive } from '@/components/CollectionArchive' - -export const ArchiveBlock: React.FC< - ArchiveBlockProps & { - id?: string - } -> = async (props) => { - const { id, categories, introContent, limit: limitFromProps, populateBy, selectedDocs } = props - - const limit = limitFromProps || 3 - - let posts: Post[] = [] - - if (populateBy === 'collection') { - const payload = await getPayload({ config: configPromise }) - - const flattenedCategories = categories?.map((category) => { - if (typeof category === 'object') return category.id - else return category - }) - - const fetchedPosts = await payload.find({ - collection: 'posts', - depth: 1, - limit, - ...(flattenedCategories && flattenedCategories.length > 0 - ? { - where: { - categories: { - in: flattenedCategories, - }, - }, - } - : {}), - }) - - posts = fetchedPosts.docs - } else { - if (selectedDocs?.length) { - const filteredSelectedPosts = selectedDocs.map((post) => { - if (typeof post.value === 'object') return post.value - }) as Post[] - - posts = filteredSelectedPosts - } - } - - return ( -
- {introContent && ( -
- -
- )} - -
- ) -} diff --git a/apps/cms/src/blocks/ArchiveBlock/config.ts b/apps/cms/src/blocks/ArchiveBlock/config.ts deleted file mode 100644 index f87a376bd51..00000000000 --- a/apps/cms/src/blocks/ArchiveBlock/config.ts +++ /dev/null @@ -1,94 +0,0 @@ -import type { Block } from 'payload' - -import { - FixedToolbarFeature, - HeadingFeature, - InlineToolbarFeature, - lexicalEditor, -} from '@payloadcms/richtext-lexical' - -export const Archive: Block = { - slug: 'archive', - interfaceName: 'ArchiveBlock', - fields: [ - { - name: 'introContent', - type: 'richText', - editor: lexicalEditor({ - features: ({ rootFeatures }) => { - return [ - ...rootFeatures, - HeadingFeature({ enabledHeadingSizes: ['h1', 'h2', 'h3', 'h4'] }), - FixedToolbarFeature(), - InlineToolbarFeature(), - ] - }, - }), - label: 'Intro Content', - }, - { - name: 'populateBy', - type: 'select', - defaultValue: 'collection', - options: [ - { - label: 'Collection', - value: 'collection', - }, - { - label: 'Individual Selection', - value: 'selection', - }, - ], - }, - { - name: 'relationTo', - type: 'select', - admin: { - condition: (_, siblingData) => siblingData.populateBy === 'collection', - }, - defaultValue: 'posts', - label: 'Collections To Show', - options: [ - { - label: 'Posts', - value: 'posts', - }, - ], - }, - { - name: 'categories', - type: 'relationship', - admin: { - condition: (_, siblingData) => siblingData.populateBy === 'collection', - }, - hasMany: true, - label: 'Categories To Show', - relationTo: 'categories', - }, - { - name: 'limit', - type: 'number', - admin: { - condition: (_, siblingData) => siblingData.populateBy === 'collection', - step: 1, - }, - defaultValue: 10, - label: 'Limit', - }, - { - name: 'selectedDocs', - type: 'relationship', - admin: { - condition: (_, siblingData) => siblingData.populateBy === 'selection', - }, - hasMany: true, - label: 'Selection', - relationTo: ['posts'], - }, - ], - labels: { - plural: 'Archives', - singular: 'Archive', - }, -} diff --git a/apps/cms/src/blocks/CallToAction/Component.tsx b/apps/cms/src/blocks/CallToAction/Component.tsx deleted file mode 100644 index 6b3771cf232..00000000000 --- a/apps/cms/src/blocks/CallToAction/Component.tsx +++ /dev/null @@ -1,23 +0,0 @@ -import React from 'react' - -import type { CallToActionBlock as CTABlockProps } from '@/payload-types' - -import RichText from '@/components/RichText' -import { CMSLink } from '@/components/Link' - -export const CallToActionBlock: React.FC = ({ links, richText }) => { - return ( -
-
-
- {richText && } -
-
- {(links || []).map(({ link }, i) => { - return - })} -
-
-
- ) -} diff --git a/apps/cms/src/blocks/CallToAction/config.ts b/apps/cms/src/blocks/CallToAction/config.ts deleted file mode 100644 index f4ffa77c6a1..00000000000 --- a/apps/cms/src/blocks/CallToAction/config.ts +++ /dev/null @@ -1,42 +0,0 @@ -import type { Block } from 'payload' - -import { - FixedToolbarFeature, - HeadingFeature, - InlineToolbarFeature, - lexicalEditor, -} from '@payloadcms/richtext-lexical' - -import { linkGroup } from '../../fields/linkGroup' - -export const CallToAction: Block = { - slug: 'cta', - interfaceName: 'CallToActionBlock', - fields: [ - { - name: 'richText', - type: 'richText', - editor: lexicalEditor({ - features: ({ rootFeatures }) => { - return [ - ...rootFeatures, - HeadingFeature({ enabledHeadingSizes: ['h1', 'h2', 'h3', 'h4'] }), - FixedToolbarFeature(), - InlineToolbarFeature(), - ] - }, - }), - label: false, - }, - linkGroup({ - appearances: ['default', 'outline'], - overrides: { - maxRows: 2, - }, - }), - ], - labels: { - plural: 'Calls to Action', - singular: 'Call to Action', - }, -} diff --git a/apps/cms/src/blocks/Code/CopyButton.tsx b/apps/cms/src/blocks/Code/CopyButton.tsx index 8d204bfffd6..546148fa740 100644 --- a/apps/cms/src/blocks/Code/CopyButton.tsx +++ b/apps/cms/src/blocks/Code/CopyButton.tsx @@ -19,7 +19,7 @@ export function CopyButton({ code }: { code: string }) {
- - )} - -
- - ) -} diff --git a/apps/cms/src/blocks/Form/Country/index.tsx b/apps/cms/src/blocks/Form/Country/index.tsx deleted file mode 100644 index 9c85b753972..00000000000 --- a/apps/cms/src/blocks/Form/Country/index.tsx +++ /dev/null @@ -1,65 +0,0 @@ -import type { CountryField } from '@payloadcms/plugin-form-builder/types' -import type { Control, FieldErrorsImpl } from 'react-hook-form' - -import { Label } from '@/components/ui/label' -import { - Select, - SelectContent, - SelectItem, - SelectTrigger, - SelectValue, -} from '@/components/ui/select' -import React from 'react' -import { Controller } from 'react-hook-form' - -import { Error } from '../Error' -import { Width } from '../Width' -import { countryOptions } from './options' - -export const Country: React.FC< - CountryField & { - control: Control - errors: Partial - } -> = ({ name, control, errors, label, required, width }) => { - return ( - - - { - const controlledValue = countryOptions.find((t) => t.value === value) - - return ( - - ) - }} - rules={{ required }} - /> - {errors[name] && } - - ) -} diff --git a/apps/cms/src/blocks/Form/Country/options.ts b/apps/cms/src/blocks/Form/Country/options.ts deleted file mode 100644 index f952c1df890..00000000000 --- a/apps/cms/src/blocks/Form/Country/options.ts +++ /dev/null @@ -1,982 +0,0 @@ -export const countryOptions = [ - { - label: 'Afghanistan', - value: 'AF', - }, - { - label: 'Åland Islands', - value: 'AX', - }, - { - label: 'Albania', - value: 'AL', - }, - { - label: 'Algeria', - value: 'DZ', - }, - { - label: 'American Samoa', - value: 'AS', - }, - { - label: 'Andorra', - value: 'AD', - }, - { - label: 'Angola', - value: 'AO', - }, - { - label: 'Anguilla', - value: 'AI', - }, - { - label: 'Antarctica', - value: 'AQ', - }, - { - label: 'Antigua and Barbuda', - value: 'AG', - }, - { - label: 'Argentina', - value: 'AR', - }, - { - label: 'Armenia', - value: 'AM', - }, - { - label: 'Aruba', - value: 'AW', - }, - { - label: 'Australia', - value: 'AU', - }, - { - label: 'Austria', - value: 'AT', - }, - { - label: 'Azerbaijan', - value: 'AZ', - }, - { - label: 'Bahamas', - value: 'BS', - }, - { - label: 'Bahrain', - value: 'BH', - }, - { - label: 'Bangladesh', - value: 'BD', - }, - { - label: 'Barbados', - value: 'BB', - }, - { - label: 'Belarus', - value: 'BY', - }, - { - label: 'Belgium', - value: 'BE', - }, - { - label: 'Belize', - value: 'BZ', - }, - { - label: 'Benin', - value: 'BJ', - }, - { - label: 'Bermuda', - value: 'BM', - }, - { - label: 'Bhutan', - value: 'BT', - }, - { - label: 'Bolivia', - value: 'BO', - }, - { - label: 'Bosnia and Herzegovina', - value: 'BA', - }, - { - label: 'Botswana', - value: 'BW', - }, - { - label: 'Bouvet Island', - value: 'BV', - }, - { - label: 'Brazil', - value: 'BR', - }, - { - label: 'British Indian Ocean Territory', - value: 'IO', - }, - { - label: 'Brunei Darussalam', - value: 'BN', - }, - { - label: 'Bulgaria', - value: 'BG', - }, - { - label: 'Burkina Faso', - value: 'BF', - }, - { - label: 'Burundi', - value: 'BI', - }, - { - label: 'Cambodia', - value: 'KH', - }, - { - label: 'Cameroon', - value: 'CM', - }, - { - label: 'Canada', - value: 'CA', - }, - { - label: 'Cape Verde', - value: 'CV', - }, - { - label: 'Cayman Islands', - value: 'KY', - }, - { - label: 'Central African Republic', - value: 'CF', - }, - { - label: 'Chad', - value: 'TD', - }, - { - label: 'Chile', - value: 'CL', - }, - { - label: 'China', - value: 'CN', - }, - { - label: 'Christmas Island', - value: 'CX', - }, - { - label: 'Cocos (Keeling) Islands', - value: 'CC', - }, - { - label: 'Colombia', - value: 'CO', - }, - { - label: 'Comoros', - value: 'KM', - }, - { - label: 'Congo', - value: 'CG', - }, - { - label: 'Congo, The Democratic Republic of the', - value: 'CD', - }, - { - label: 'Cook Islands', - value: 'CK', - }, - { - label: 'Costa Rica', - value: 'CR', - }, - { - label: "Cote D'Ivoire", - value: 'CI', - }, - { - label: 'Croatia', - value: 'HR', - }, - { - label: 'Cuba', - value: 'CU', - }, - { - label: 'Cyprus', - value: 'CY', - }, - { - label: 'Czech Republic', - value: 'CZ', - }, - { - label: 'Denmark', - value: 'DK', - }, - { - label: 'Djibouti', - value: 'DJ', - }, - { - label: 'Dominica', - value: 'DM', - }, - { - label: 'Dominican Republic', - value: 'DO', - }, - { - label: 'Ecuador', - value: 'EC', - }, - { - label: 'Egypt', - value: 'EG', - }, - { - label: 'El Salvador', - value: 'SV', - }, - { - label: 'Equatorial Guinea', - value: 'GQ', - }, - { - label: 'Eritrea', - value: 'ER', - }, - { - label: 'Estonia', - value: 'EE', - }, - { - label: 'Ethiopia', - value: 'ET', - }, - { - label: 'Falkland Islands (Malvinas)', - value: 'FK', - }, - { - label: 'Faroe Islands', - value: 'FO', - }, - { - label: 'Fiji', - value: 'FJ', - }, - { - label: 'Finland', - value: 'FI', - }, - { - label: 'France', - value: 'FR', - }, - { - label: 'French Guiana', - value: 'GF', - }, - { - label: 'French Polynesia', - value: 'PF', - }, - { - label: 'French Southern Territories', - value: 'TF', - }, - { - label: 'Gabon', - value: 'GA', - }, - { - label: 'Gambia', - value: 'GM', - }, - { - label: 'Georgia', - value: 'GE', - }, - { - label: 'Germany', - value: 'DE', - }, - { - label: 'Ghana', - value: 'GH', - }, - { - label: 'Gibraltar', - value: 'GI', - }, - { - label: 'Greece', - value: 'GR', - }, - { - label: 'Greenland', - value: 'GL', - }, - { - label: 'Grenada', - value: 'GD', - }, - { - label: 'Guadeloupe', - value: 'GP', - }, - { - label: 'Guam', - value: 'GU', - }, - { - label: 'Guatemala', - value: 'GT', - }, - { - label: 'Guernsey', - value: 'GG', - }, - { - label: 'Guinea', - value: 'GN', - }, - { - label: 'Guinea-Bissau', - value: 'GW', - }, - { - label: 'Guyana', - value: 'GY', - }, - { - label: 'Haiti', - value: 'HT', - }, - { - label: 'Heard Island and Mcdonald Islands', - value: 'HM', - }, - { - label: 'Holy See (Vatican City State)', - value: 'VA', - }, - { - label: 'Honduras', - value: 'HN', - }, - { - label: 'Hong Kong', - value: 'HK', - }, - { - label: 'Hungary', - value: 'HU', - }, - { - label: 'Iceland', - value: 'IS', - }, - { - label: 'India', - value: 'IN', - }, - { - label: 'Indonesia', - value: 'ID', - }, - { - label: 'Iran, Islamic Republic Of', - value: 'IR', - }, - { - label: 'Iraq', - value: 'IQ', - }, - { - label: 'Ireland', - value: 'IE', - }, - { - label: 'Isle of Man', - value: 'IM', - }, - { - label: 'Israel', - value: 'IL', - }, - { - label: 'Italy', - value: 'IT', - }, - { - label: 'Jamaica', - value: 'JM', - }, - { - label: 'Japan', - value: 'JP', - }, - { - label: 'Jersey', - value: 'JE', - }, - { - label: 'Jordan', - value: 'JO', - }, - { - label: 'Kazakhstan', - value: 'KZ', - }, - { - label: 'Kenya', - value: 'KE', - }, - { - label: 'Kiribati', - value: 'KI', - }, - { - label: "Democratic People's Republic of Korea", - value: 'KP', - }, - { - label: 'Korea, Republic of', - value: 'KR', - }, - { - label: 'Kosovo', - value: 'XK', - }, - { - label: 'Kuwait', - value: 'KW', - }, - { - label: 'Kyrgyzstan', - value: 'KG', - }, - { - label: "Lao People's Democratic Republic", - value: 'LA', - }, - { - label: 'Latvia', - value: 'LV', - }, - { - label: 'Lebanon', - value: 'LB', - }, - { - label: 'Lesotho', - value: 'LS', - }, - { - label: 'Liberia', - value: 'LR', - }, - { - label: 'Libyan Arab Jamahiriya', - value: 'LY', - }, - { - label: 'Liechtenstein', - value: 'LI', - }, - { - label: 'Lithuania', - value: 'LT', - }, - { - label: 'Luxembourg', - value: 'LU', - }, - { - label: 'Macao', - value: 'MO', - }, - { - label: 'Macedonia, The Former Yugoslav Republic of', - value: 'MK', - }, - { - label: 'Madagascar', - value: 'MG', - }, - { - label: 'Malawi', - value: 'MW', - }, - { - label: 'Malaysia', - value: 'MY', - }, - { - label: 'Maldives', - value: 'MV', - }, - { - label: 'Mali', - value: 'ML', - }, - { - label: 'Malta', - value: 'MT', - }, - { - label: 'Marshall Islands', - value: 'MH', - }, - { - label: 'Martinique', - value: 'MQ', - }, - { - label: 'Mauritania', - value: 'MR', - }, - { - label: 'Mauritius', - value: 'MU', - }, - { - label: 'Mayotte', - value: 'YT', - }, - { - label: 'Mexico', - value: 'MX', - }, - { - label: 'Micronesia, Federated States of', - value: 'FM', - }, - { - label: 'Moldova, Republic of', - value: 'MD', - }, - { - label: 'Monaco', - value: 'MC', - }, - { - label: 'Mongolia', - value: 'MN', - }, - { - label: 'Montenegro', - value: 'ME', - }, - { - label: 'Montserrat', - value: 'MS', - }, - { - label: 'Morocco', - value: 'MA', - }, - { - label: 'Mozambique', - value: 'MZ', - }, - { - label: 'Myanmar', - value: 'MM', - }, - { - label: 'Namibia', - value: 'NA', - }, - { - label: 'Nauru', - value: 'NR', - }, - { - label: 'Nepal', - value: 'NP', - }, - { - label: 'Netherlands', - value: 'NL', - }, - { - label: 'Netherlands Antilles', - value: 'AN', - }, - { - label: 'New Caledonia', - value: 'NC', - }, - { - label: 'New Zealand', - value: 'NZ', - }, - { - label: 'Nicaragua', - value: 'NI', - }, - { - label: 'Niger', - value: 'NE', - }, - { - label: 'Nigeria', - value: 'NG', - }, - { - label: 'Niue', - value: 'NU', - }, - { - label: 'Norfolk Island', - value: 'NF', - }, - { - label: 'Northern Mariana Islands', - value: 'MP', - }, - { - label: 'Norway', - value: 'NO', - }, - { - label: 'Oman', - value: 'OM', - }, - { - label: 'Pakistan', - value: 'PK', - }, - { - label: 'Palau', - value: 'PW', - }, - { - label: 'Palestinian Territory, Occupied', - value: 'PS', - }, - { - label: 'Panama', - value: 'PA', - }, - { - label: 'Papua New Guinea', - value: 'PG', - }, - { - label: 'Paraguay', - value: 'PY', - }, - { - label: 'Peru', - value: 'PE', - }, - { - label: 'Philippines', - value: 'PH', - }, - { - label: 'Pitcairn', - value: 'PN', - }, - { - label: 'Poland', - value: 'PL', - }, - { - label: 'Portugal', - value: 'PT', - }, - { - label: 'Puerto Rico', - value: 'PR', - }, - { - label: 'Qatar', - value: 'QA', - }, - { - label: 'Reunion', - value: 'RE', - }, - { - label: 'Romania', - value: 'RO', - }, - { - label: 'Russian Federation', - value: 'RU', - }, - { - label: 'Rwanda', - value: 'RW', - }, - { - label: 'Saint Helena', - value: 'SH', - }, - { - label: 'Saint Kitts and Nevis', - value: 'KN', - }, - { - label: 'Saint Lucia', - value: 'LC', - }, - { - label: 'Saint Pierre and Miquelon', - value: 'PM', - }, - { - label: 'Saint Vincent and the Grenadines', - value: 'VC', - }, - { - label: 'Samoa', - value: 'WS', - }, - { - label: 'San Marino', - value: 'SM', - }, - { - label: 'Sao Tome and Principe', - value: 'ST', - }, - { - label: 'Saudi Arabia', - value: 'SA', - }, - { - label: 'Senegal', - value: 'SN', - }, - { - label: 'Serbia', - value: 'RS', - }, - { - label: 'Seychelles', - value: 'SC', - }, - { - label: 'Sierra Leone', - value: 'SL', - }, - { - label: 'Singapore', - value: 'SG', - }, - { - label: 'Slovakia', - value: 'SK', - }, - { - label: 'Slovenia', - value: 'SI', - }, - { - label: 'Solomon Islands', - value: 'SB', - }, - { - label: 'Somalia', - value: 'SO', - }, - { - label: 'South Africa', - value: 'ZA', - }, - { - label: 'South Georgia and the South Sandwich Islands', - value: 'GS', - }, - { - label: 'Spain', - value: 'ES', - }, - { - label: 'Sri Lanka', - value: 'LK', - }, - { - label: 'Sudan', - value: 'SD', - }, - { - label: 'Suriname', - value: 'SR', - }, - { - label: 'Svalbard and Jan Mayen', - value: 'SJ', - }, - { - label: 'Swaziland', - value: 'SZ', - }, - { - label: 'Sweden', - value: 'SE', - }, - { - label: 'Switzerland', - value: 'CH', - }, - { - label: 'Syrian Arab Republic', - value: 'SY', - }, - { - label: 'Taiwan', - value: 'TW', - }, - { - label: 'Tajikistan', - value: 'TJ', - }, - { - label: 'Tanzania, United Republic of', - value: 'TZ', - }, - { - label: 'Thailand', - value: 'TH', - }, - { - label: 'Timor-Leste', - value: 'TL', - }, - { - label: 'Togo', - value: 'TG', - }, - { - label: 'Tokelau', - value: 'TK', - }, - { - label: 'Tonga', - value: 'TO', - }, - { - label: 'Trinidad and Tobago', - value: 'TT', - }, - { - label: 'Tunisia', - value: 'TN', - }, - { - label: 'Turkey', - value: 'TR', - }, - { - label: 'Turkmenistan', - value: 'TM', - }, - { - label: 'Turks and Caicos Islands', - value: 'TC', - }, - { - label: 'Tuvalu', - value: 'TV', - }, - { - label: 'Uganda', - value: 'UG', - }, - { - label: 'Ukraine', - value: 'UA', - }, - { - label: 'United Arab Emirates', - value: 'AE', - }, - { - label: 'United Kingdom', - value: 'GB', - }, - { - label: 'United States', - value: 'US', - }, - { - label: 'United States Minor Outlying Islands', - value: 'UM', - }, - { - label: 'Uruguay', - value: 'UY', - }, - { - label: 'Uzbekistan', - value: 'UZ', - }, - { - label: 'Vanuatu', - value: 'VU', - }, - { - label: 'Venezuela', - value: 'VE', - }, - { - label: 'Viet Nam', - value: 'VN', - }, - { - label: 'Virgin Islands, British', - value: 'VG', - }, - { - label: 'Virgin Islands, U.S.', - value: 'VI', - }, - { - label: 'Wallis and Futuna', - value: 'WF', - }, - { - label: 'Western Sahara', - value: 'EH', - }, - { - label: 'Yemen', - value: 'YE', - }, - { - label: 'Zambia', - value: 'ZM', - }, - { - label: 'Zimbabwe', - value: 'ZW', - }, -] diff --git a/apps/cms/src/blocks/Form/Email/index.tsx b/apps/cms/src/blocks/Form/Email/index.tsx deleted file mode 100644 index fc9fd2804cd..00000000000 --- a/apps/cms/src/blocks/Form/Email/index.tsx +++ /dev/null @@ -1,38 +0,0 @@ -import type { EmailField } from '@payloadcms/plugin-form-builder/types' -import type { FieldErrorsImpl, FieldValues, UseFormRegister } from 'react-hook-form' - -import { Input } from '@/components/ui/input' -import { Label } from '@/components/ui/label' -import React from 'react' - -import { Error } from '../Error' -import { Width } from '../Width' - -export const Email: React.FC< - EmailField & { - errors: Partial - register: UseFormRegister - } -> = ({ name, defaultValue, errors, label, register, required, width }) => { - return ( - - - - - {errors[name] && } - - ) -} diff --git a/apps/cms/src/blocks/Form/Error/index.tsx b/apps/cms/src/blocks/Form/Error/index.tsx deleted file mode 100644 index a7b9e47e805..00000000000 --- a/apps/cms/src/blocks/Form/Error/index.tsx +++ /dev/null @@ -1,15 +0,0 @@ -'use client' - -import * as React from 'react' -import { useFormContext } from 'react-hook-form' - -export const Error = ({ name }: { name: string }) => { - const { - formState: { errors }, - } = useFormContext() - return ( -
- {(errors[name]?.message as string) || 'This field is required'} -
- ) -} diff --git a/apps/cms/src/blocks/Form/Message/index.tsx b/apps/cms/src/blocks/Form/Message/index.tsx deleted file mode 100644 index 5924cf95f40..00000000000 --- a/apps/cms/src/blocks/Form/Message/index.tsx +++ /dev/null @@ -1,13 +0,0 @@ -import RichText from '@/components/RichText' -import React from 'react' - -import { Width } from '../Width' -import { SerializedEditorState } from '@payloadcms/richtext-lexical/lexical' - -export const Message: React.FC<{ message: SerializedEditorState }> = ({ message }) => { - return ( - - {message && } - - ) -} diff --git a/apps/cms/src/blocks/Form/Number/index.tsx b/apps/cms/src/blocks/Form/Number/index.tsx deleted file mode 100644 index f26e54a4486..00000000000 --- a/apps/cms/src/blocks/Form/Number/index.tsx +++ /dev/null @@ -1,36 +0,0 @@ -import type { TextField } from '@payloadcms/plugin-form-builder/types' -import type { FieldErrorsImpl, FieldValues, UseFormRegister } from 'react-hook-form' - -import { Input } from '@/components/ui/input' -import { Label } from '@/components/ui/label' -import React from 'react' - -import { Error } from '../Error' -import { Width } from '../Width' -export const Number: React.FC< - TextField & { - errors: Partial - register: UseFormRegister - } -> = ({ name, defaultValue, errors, label, register, required, width }) => { - return ( - - - - {errors[name] && } - - ) -} diff --git a/apps/cms/src/blocks/Form/Select/index.tsx b/apps/cms/src/blocks/Form/Select/index.tsx deleted file mode 100644 index 30c0e8312ac..00000000000 --- a/apps/cms/src/blocks/Form/Select/index.tsx +++ /dev/null @@ -1,63 +0,0 @@ -import type { SelectField } from '@payloadcms/plugin-form-builder/types' -import type { Control, FieldErrorsImpl } from 'react-hook-form' - -import { Label } from '@/components/ui/label' -import { - Select as SelectComponent, - SelectContent, - SelectItem, - SelectTrigger, - SelectValue, -} from '@/components/ui/select' -import React from 'react' -import { Controller } from 'react-hook-form' - -import { Error } from '../Error' -import { Width } from '../Width' - -export const Select: React.FC< - SelectField & { - control: Control - errors: Partial - } -> = ({ name, control, errors, label, options, required, width, defaultValue }) => { - return ( - - - { - const controlledValue = options.find((t) => t.value === value) - - return ( - onChange(val)} value={controlledValue?.value}> - - - - - {options.map(({ label, value }) => { - return ( - - {label} - - ) - })} - - - ) - }} - rules={{ required }} - /> - {errors[name] && } - - ) -} diff --git a/apps/cms/src/blocks/Form/State/index.tsx b/apps/cms/src/blocks/Form/State/index.tsx deleted file mode 100644 index 29e49cae0c3..00000000000 --- a/apps/cms/src/blocks/Form/State/index.tsx +++ /dev/null @@ -1,64 +0,0 @@ -import type { StateField } from '@payloadcms/plugin-form-builder/types' -import type { Control, FieldErrorsImpl } from 'react-hook-form' - -import { Label } from '@/components/ui/label' -import { - Select, - SelectContent, - SelectItem, - SelectTrigger, - SelectValue, -} from '@/components/ui/select' -import React from 'react' -import { Controller } from 'react-hook-form' - -import { Error } from '../Error' -import { Width } from '../Width' -import { stateOptions } from './options' - -export const State: React.FC< - StateField & { - control: Control - errors: Partial - } -> = ({ name, control, errors, label, required, width }) => { - return ( - - - { - const controlledValue = stateOptions.find((t) => t.value === value) - - return ( - - ) - }} - rules={{ required }} - /> - {errors[name] && } - - ) -} diff --git a/apps/cms/src/blocks/Form/State/options.ts b/apps/cms/src/blocks/Form/State/options.ts deleted file mode 100644 index 8dff991e7ab..00000000000 --- a/apps/cms/src/blocks/Form/State/options.ts +++ /dev/null @@ -1,52 +0,0 @@ -export const stateOptions = [ - { label: 'Alabama', value: 'AL' }, - { label: 'Alaska', value: 'AK' }, - { label: 'Arizona', value: 'AZ' }, - { label: 'Arkansas', value: 'AR' }, - { label: 'California', value: 'CA' }, - { label: 'Colorado', value: 'CO' }, - { label: 'Connecticut', value: 'CT' }, - { label: 'Delaware', value: 'DE' }, - { label: 'Florida', value: 'FL' }, - { label: 'Georgia', value: 'GA' }, - { label: 'Hawaii', value: 'HI' }, - { label: 'Idaho', value: 'ID' }, - { label: 'Illinois', value: 'IL' }, - { label: 'Indiana', value: 'IN' }, - { label: 'Iowa', value: 'IA' }, - { label: 'Kansas', value: 'KS' }, - { label: 'Kentucky', value: 'KY' }, - { label: 'Louisiana', value: 'LA' }, - { label: 'Maine', value: 'ME' }, - { label: 'Maryland', value: 'MD' }, - { label: 'Massachusetts', value: 'MA' }, - { label: 'Michigan', value: 'MI' }, - { label: 'Minnesota', value: 'MN' }, - { label: 'Mississippi', value: 'MS' }, - { label: 'Missouri', value: 'MO' }, - { label: 'Montana', value: 'MT' }, - { label: 'Nebraska', value: 'NE' }, - { label: 'Nevada', value: 'NV' }, - { label: 'New Hampshire', value: 'NH' }, - { label: 'New Jersey', value: 'NJ' }, - { label: 'New Mexico', value: 'NM' }, - { label: 'New York', value: 'NY' }, - { label: 'North Carolina', value: 'NC' }, - { label: 'North Dakota', value: 'ND' }, - { label: 'Ohio', value: 'OH' }, - { label: 'Oklahoma', value: 'OK' }, - { label: 'Oregon', value: 'OR' }, - { label: 'Pennsylvania', value: 'PA' }, - { label: 'Rhode Island', value: 'RI' }, - { label: 'South Carolina', value: 'SC' }, - { label: 'South Dakota', value: 'SD' }, - { label: 'Tennessee', value: 'TN' }, - { label: 'Texas', value: 'TX' }, - { label: 'Utah', value: 'UT' }, - { label: 'Vermont', value: 'VT' }, - { label: 'Virginia', value: 'VA' }, - { label: 'Washington', value: 'WA' }, - { label: 'West Virginia', value: 'WV' }, - { label: 'Wisconsin', value: 'WI' }, - { label: 'Wyoming', value: 'WY' }, -] diff --git a/apps/cms/src/blocks/Form/Text/index.tsx b/apps/cms/src/blocks/Form/Text/index.tsx deleted file mode 100644 index be1e0ff12bd..00000000000 --- a/apps/cms/src/blocks/Form/Text/index.tsx +++ /dev/null @@ -1,32 +0,0 @@ -import type { TextField } from '@payloadcms/plugin-form-builder/types' -import type { FieldErrorsImpl, FieldValues, UseFormRegister } from 'react-hook-form' - -import { Input } from '@/components/ui/input' -import { Label } from '@/components/ui/label' -import React from 'react' - -import { Error } from '../Error' -import { Width } from '../Width' - -export const Text: React.FC< - TextField & { - errors: Partial - register: UseFormRegister - } -> = ({ name, defaultValue, errors, label, register, required, width }) => { - return ( - - - - {errors[name] && } - - ) -} diff --git a/apps/cms/src/blocks/Form/Textarea/index.tsx b/apps/cms/src/blocks/Form/Textarea/index.tsx deleted file mode 100644 index ecb6e21afdb..00000000000 --- a/apps/cms/src/blocks/Form/Textarea/index.tsx +++ /dev/null @@ -1,40 +0,0 @@ -import type { TextField } from '@payloadcms/plugin-form-builder/types' -import type { FieldErrorsImpl, FieldValues, UseFormRegister } from 'react-hook-form' - -import { Label } from '@/components/ui/label' -import { Textarea as TextAreaComponent } from '@/components/ui/textarea' -import React from 'react' - -import { Error } from '../Error' -import { Width } from '../Width' - -export const Textarea: React.FC< - TextField & { - errors: Partial - register: UseFormRegister - rows?: number - } -> = ({ name, defaultValue, errors, label, register, required, rows = 3, width }) => { - return ( - - - - - - {errors[name] && } - - ) -} diff --git a/apps/cms/src/blocks/Form/Width/index.tsx b/apps/cms/src/blocks/Form/Width/index.tsx deleted file mode 100644 index bcc51a33333..00000000000 --- a/apps/cms/src/blocks/Form/Width/index.tsx +++ /dev/null @@ -1,13 +0,0 @@ -import * as React from 'react' - -export const Width: React.FC<{ - children: React.ReactNode - className?: string - width?: number | string -}> = ({ children, className, width }) => { - return ( -
- {children} -
- ) -} diff --git a/apps/cms/src/blocks/Form/config.ts b/apps/cms/src/blocks/Form/config.ts deleted file mode 100644 index 5334289f3a0..00000000000 --- a/apps/cms/src/blocks/Form/config.ts +++ /dev/null @@ -1,51 +0,0 @@ -import type { Block } from 'payload' - -import { - FixedToolbarFeature, - HeadingFeature, - InlineToolbarFeature, - lexicalEditor, -} from '@payloadcms/richtext-lexical' - -export const FormBlock: Block = { - slug: 'formBlock', - interfaceName: 'FormBlock', - fields: [ - { - name: 'form', - type: 'relationship', - relationTo: 'forms', - required: true, - }, - { - name: 'enableIntro', - type: 'checkbox', - label: 'Enable Intro Content', - }, - { - name: 'introContent', - type: 'richText', - admin: { - condition: (_, { enableIntro }) => Boolean(enableIntro), - }, - editor: lexicalEditor({ - features: ({ rootFeatures }) => { - return [ - ...rootFeatures, - HeadingFeature({ enabledHeadingSizes: ['h1', 'h2', 'h3', 'h4'] }), - FixedToolbarFeature(), - InlineToolbarFeature(), - ] - }, - }), - label: 'Intro Content', - }, - ], - graphQL: { - singularName: 'FormBlock', - }, - labels: { - plural: 'Form Blocks', - singular: 'Form Block', - }, -} diff --git a/apps/cms/src/blocks/Form/fields.tsx b/apps/cms/src/blocks/Form/fields.tsx deleted file mode 100644 index fa660f7e398..00000000000 --- a/apps/cms/src/blocks/Form/fields.tsx +++ /dev/null @@ -1,21 +0,0 @@ -import { Checkbox } from './Checkbox' -import { Country } from './Country' -import { Email } from './Email' -import { Message } from './Message' -import { Number } from './Number' -import { Select } from './Select' -import { State } from './State' -import { Text } from './Text' -import { Textarea } from './Textarea' - -export const fields = { - checkbox: Checkbox, - country: Country, - email: Email, - message: Message, - number: Number, - select: Select, - state: State, - text: Text, - textarea: Textarea, -} diff --git a/apps/cms/src/blocks/RelatedPosts/Component.tsx b/apps/cms/src/blocks/RelatedPosts/Component.tsx deleted file mode 100644 index 8d8c9798b20..00000000000 --- a/apps/cms/src/blocks/RelatedPosts/Component.tsx +++ /dev/null @@ -1,32 +0,0 @@ -import clsx from 'clsx' -import React from 'react' -import RichText from '@/components/RichText' - -import type { Post } from '@/payload-types' - -import { Card } from '../../components/Card' -import { SerializedEditorState } from '@payloadcms/richtext-lexical/lexical' - -export type RelatedPostsProps = { - className?: string - docs?: Post[] - introContent?: SerializedEditorState -} - -export const RelatedPosts: React.FC = (props) => { - const { className, docs, introContent } = props - - return ( -
- {introContent && } - -
- {docs?.map((doc, index) => { - if (typeof doc === 'string') return null - - return - })} -
-
- ) -} diff --git a/apps/cms/src/blocks/RenderBlocks.tsx b/apps/cms/src/blocks/RenderBlocks.tsx deleted file mode 100644 index c84634a64ad..00000000000 --- a/apps/cms/src/blocks/RenderBlocks.tsx +++ /dev/null @@ -1,51 +0,0 @@ -import React, { Fragment } from 'react' - -import type { Page } from '@/payload-types' - -import { ArchiveBlock } from '@/blocks/ArchiveBlock/Component' -import { CallToActionBlock } from '@/blocks/CallToAction/Component' -import { ContentBlock } from '@/blocks/Content/Component' -import { FormBlock } from '@/blocks/Form/Component' -import { MediaBlock } from '@/blocks/MediaBlock/Component' - -const blockComponents = { - archive: ArchiveBlock, - content: ContentBlock, - cta: CallToActionBlock, - formBlock: FormBlock, - mediaBlock: MediaBlock, -} - -export const RenderBlocks: React.FC<{ - blocks: Page['layout'][0][] -}> = (props) => { - const { blocks } = props - - const hasBlocks = blocks && Array.isArray(blocks) && blocks.length > 0 - - if (hasBlocks) { - return ( - - {blocks.map((block, index) => { - const { blockType } = block - - if (blockType && blockType in blockComponents) { - const Block = blockComponents[blockType] - - if (Block) { - return ( -
- {/* @ts-expect-error there may be some mismatch between the expected types here */} - -
- ) - } - } - return null - })} -
- ) - } - - return null -} diff --git a/apps/cms/src/collections/Authors.ts b/apps/cms/src/collections/Authors.ts index f9ba90abc79..44cb98321cd 100644 --- a/apps/cms/src/collections/Authors.ts +++ b/apps/cms/src/collections/Authors.ts @@ -1,4 +1,4 @@ -import { CollectionConfig } from 'payload' +import type { CollectionConfig } from 'payload' export const Authors: CollectionConfig = { slug: 'authors', diff --git a/apps/cms/src/collections/Categories.ts b/apps/cms/src/collections/Categories.ts index 6203fb9bed4..7fc90e96c2a 100644 --- a/apps/cms/src/collections/Categories.ts +++ b/apps/cms/src/collections/Categories.ts @@ -1,4 +1,4 @@ -import { CollectionConfig } from 'payload' +import type { CollectionConfig } from 'payload' export const Categories: CollectionConfig = { slug: 'categories', diff --git a/apps/cms/src/collections/Customers/hooks/revalidateCustomer.ts b/apps/cms/src/collections/Customers/hooks/revalidateCustomer.ts index 5e6f9cbe797..df9cc3626e7 100644 --- a/apps/cms/src/collections/Customers/hooks/revalidateCustomer.ts +++ b/apps/cms/src/collections/Customers/hooks/revalidateCustomer.ts @@ -1,10 +1,10 @@ import type { CollectionAfterChangeHook, CollectionAfterDeleteHook } from 'payload' -import { revalidatePath, revalidateTag } from 'next/cache' +// Avoid importing next/cache at module scope so migrations can run in a plain Node env import type { Customer } from '../../../payload-types' -export const revalidateCustomer: CollectionAfterChangeHook = ({ +export const revalidateCustomer: CollectionAfterChangeHook = async ({ doc, previousDoc, req: { payload, context }, @@ -14,9 +14,13 @@ export const revalidateCustomer: CollectionAfterChangeHook = ({ const path = `/customers/${doc.slug}` payload.logger.info(`Revalidating event at path: ${path}`) - - revalidatePath(path) - revalidateTag('customers-sitemap') + try { + const { revalidatePath, revalidateTag } = await import('next/cache') + revalidatePath(path) + revalidateTag('customers-sitemap') + } catch { + // no-op when not running inside Next runtime (e.g., during payload migrate) + } } // If the event was previously published, we need to revalidate the old path @@ -25,22 +29,27 @@ export const revalidateCustomer: CollectionAfterChangeHook = ({ payload.logger.info(`Revalidating old event at path: ${oldPath}`) - revalidatePath(oldPath) - revalidateTag('customers-sitemap') + try { + const { revalidatePath, revalidateTag } = await import('next/cache') + revalidatePath(oldPath) + revalidateTag('customers-sitemap') + } catch {} } } return doc } -export const revalidateDelete: CollectionAfterDeleteHook = ({ +export const revalidateDelete: CollectionAfterDeleteHook = async ({ doc, req: { context }, }) => { if (!context.disableRevalidate) { const path = `/customers/${doc?.slug}` - - revalidatePath(path) - revalidateTag('customers-sitemap') + try { + const { revalidatePath, revalidateTag } = await import('next/cache') + revalidatePath(path) + revalidateTag('customers-sitemap') + } catch {} } return doc diff --git a/apps/cms/src/collections/Customers/index.ts b/apps/cms/src/collections/Customers/index.ts index 2fad829f378..a7108d200e3 100644 --- a/apps/cms/src/collections/Customers/index.ts +++ b/apps/cms/src/collections/Customers/index.ts @@ -9,15 +9,15 @@ import { lexicalEditor, } from '@payloadcms/richtext-lexical' -import { isAnyone } from '@/access/isAnyone' -import { isAuthenticated } from '@/access/isAuthenticated' +import { isAnyone } from '../../access/isAnyone.ts' +import { isAuthenticated } from '../../access/isAuthenticated.ts' -import { Banner } from '../../blocks/Banner/config' -import { Code } from '../../blocks/Code/config' -import { MediaBlock } from '../../blocks/MediaBlock/config' -import { Quote } from '../../blocks/Quote/config' -import { YouTube } from '../../blocks/YouTube/config' -import { revalidateDelete, revalidateCustomer } from './hooks/revalidateCustomer' +import { Banner } from '../../blocks/Banner/config.ts' +import { Code } from '../../blocks/Code/config.ts' +import { MediaBlock } from '../../blocks/MediaBlock/config.ts' +import { Quote } from '../../blocks/Quote/config.ts' +import { YouTube } from '../../blocks/YouTube/config.ts' +import { revalidateDelete, revalidateCustomer } from './hooks/revalidateCustomer.ts' import { MetaDescriptionField, @@ -26,7 +26,7 @@ import { OverviewField, PreviewField, } from '@payloadcms/plugin-seo/fields' -import { slugField } from '@/fields/slug' +import { slugField } from '../../fields/slug/index.ts' const industryOptions = [ { label: 'Healthcare', value: 'healthcare' }, diff --git a/apps/cms/src/collections/Events/hooks/revalidateEvent.ts b/apps/cms/src/collections/Events/hooks/revalidateEvent.ts index 792860785eb..98d2a20b868 100644 --- a/apps/cms/src/collections/Events/hooks/revalidateEvent.ts +++ b/apps/cms/src/collections/Events/hooks/revalidateEvent.ts @@ -1,10 +1,10 @@ import type { CollectionAfterChangeHook, CollectionAfterDeleteHook } from 'payload' -import { revalidatePath, revalidateTag } from 'next/cache' +// Avoid importing next/cache at module scope so migrations can run in a plain Node env import type { Event } from '../../../payload-types' -export const revalidateEvent: CollectionAfterChangeHook = ({ +export const revalidateEvent: CollectionAfterChangeHook = async ({ doc, previousDoc, req: { payload, context }, @@ -14,9 +14,11 @@ export const revalidateEvent: CollectionAfterChangeHook = ({ const path = `/events/${doc.slug}` payload.logger.info(`Revalidating event at path: ${path}`) - - revalidatePath(path) - revalidateTag('events-sitemap') + try { + const { revalidatePath, revalidateTag } = await import('next/cache') + revalidatePath(path) + revalidateTag('events-sitemap') + } catch {} } // If the event was previously published, we need to revalidate the old path @@ -25,19 +27,27 @@ export const revalidateEvent: CollectionAfterChangeHook = ({ payload.logger.info(`Revalidating old event at path: ${oldPath}`) - revalidatePath(oldPath) - revalidateTag('events-sitemap') + try { + const { revalidatePath, revalidateTag } = await import('next/cache') + revalidatePath(oldPath) + revalidateTag('events-sitemap') + } catch {} } } return doc } -export const revalidateDelete: CollectionAfterDeleteHook = ({ doc, req: { context } }) => { +export const revalidateDelete: CollectionAfterDeleteHook = async ({ + doc, + req: { context }, +}) => { if (!context.disableRevalidate) { const path = `/events/${doc?.slug}` - - revalidatePath(path) - revalidateTag('events-sitemap') + try { + const { revalidatePath, revalidateTag } = await import('next/cache') + revalidatePath(path) + revalidateTag('events-sitemap') + } catch {} } return doc diff --git a/apps/cms/src/collections/Events/index.ts b/apps/cms/src/collections/Events/index.ts index be5f1841c98..ad99e840e2d 100644 --- a/apps/cms/src/collections/Events/index.ts +++ b/apps/cms/src/collections/Events/index.ts @@ -9,15 +9,15 @@ import { lexicalEditor, } from '@payloadcms/richtext-lexical' -import { isAnyone } from '@/access/isAnyone' -import { isAuthenticated } from '@/access/isAuthenticated' +import { isAnyone } from '../../access/isAnyone.ts' +import { isAuthenticated } from '../../access/isAuthenticated.ts' -import { Banner } from '@/blocks/Banner/config' -import { Code } from '@/blocks/Code/config' -import { MediaBlock } from '@/blocks/MediaBlock/config' -import { Quote } from '@/blocks/Quote/config' -import { YouTube } from '@/blocks/YouTube/config' -import { revalidateDelete, revalidateEvent } from './hooks/revalidateEvent' +import { Banner } from '../../blocks/Banner/config.ts' +import { Code } from '../../blocks/Code/config.ts' +import { MediaBlock } from '../../blocks/MediaBlock/config.ts' +import { Quote } from '../../blocks/Quote/config.ts' +import { YouTube } from '../../blocks/YouTube/config.ts' +import { revalidateDelete, revalidateEvent } from './hooks/revalidateEvent.ts' import { MetaDescriptionField, @@ -26,8 +26,8 @@ import { OverviewField, PreviewField, } from '@payloadcms/plugin-seo/fields' -import { slugField } from '@/fields/slug' -import { timezoneOptions } from '../../utilities/timezones' +import { slugField } from '../../fields/slug/index.ts' +import { timezoneOptions } from '../../utilities/timezones.ts' const eventTypeOptions = [ { label: 'Conference', value: 'conference' }, diff --git a/apps/cms/src/collections/Media.ts b/apps/cms/src/collections/Media.ts index 9e82a3cecd4..9b8f9c70ae8 100644 --- a/apps/cms/src/collections/Media.ts +++ b/apps/cms/src/collections/Media.ts @@ -5,8 +5,8 @@ import { lexicalEditor, } from '@payloadcms/richtext-lexical' -import { isAnyone } from '@/access/isAnyone' -import { isAuthenticated } from '@/access/isAuthenticated' +import { isAnyone } from '../access/isAnyone.ts' +import { isAuthenticated } from '../access/isAuthenticated.ts' export const Media: CollectionConfig = { slug: 'media', diff --git a/apps/cms/src/collections/Posts/hooks/populateAuthors.ts b/apps/cms/src/collections/Posts/hooks/populateAuthors.ts index 8617cb1fd02..6a57e36534a 100644 --- a/apps/cms/src/collections/Posts/hooks/populateAuthors.ts +++ b/apps/cms/src/collections/Posts/hooks/populateAuthors.ts @@ -1,5 +1,5 @@ import type { CollectionAfterReadHook } from 'payload' -import { Author } from 'src/payload-types' +import type { Author } from '../../../payload-types' export const populateAuthors: CollectionAfterReadHook = async ({ doc, req: { payload } }) => { if (doc?.authors && doc?.authors?.length > 0) { diff --git a/apps/cms/src/collections/Posts/hooks/revalidatePost.ts b/apps/cms/src/collections/Posts/hooks/revalidatePost.ts index ff43752852e..018f7b9cb41 100644 --- a/apps/cms/src/collections/Posts/hooks/revalidatePost.ts +++ b/apps/cms/src/collections/Posts/hooks/revalidatePost.ts @@ -1,10 +1,10 @@ import type { CollectionAfterChangeHook, CollectionAfterDeleteHook } from 'payload' -import { revalidatePath, revalidateTag } from 'next/cache' +// Avoid importing next/cache at module scope so migrations can run in a plain Node env import type { Post } from '../../../payload-types' -export const revalidatePost: CollectionAfterChangeHook = ({ +export const revalidatePost: CollectionAfterChangeHook = async ({ doc, previousDoc, req: { payload, context }, @@ -14,9 +14,11 @@ export const revalidatePost: CollectionAfterChangeHook = ({ const path = `/posts/${doc.slug}` payload.logger.info(`Revalidating post at path: ${path}`) - - revalidatePath(path) - revalidateTag('posts-sitemap') + try { + const { revalidatePath, revalidateTag } = await import('next/cache') + revalidatePath(path) + revalidateTag('posts-sitemap') + } catch {} } // If the post was previously published, we need to revalidate the old path @@ -25,19 +27,27 @@ export const revalidatePost: CollectionAfterChangeHook = ({ payload.logger.info(`Revalidating old post at path: ${oldPath}`) - revalidatePath(oldPath) - revalidateTag('posts-sitemap') + try { + const { revalidatePath, revalidateTag } = await import('next/cache') + revalidatePath(oldPath) + revalidateTag('posts-sitemap') + } catch {} } } return doc } -export const revalidateDelete: CollectionAfterDeleteHook = ({ doc, req: { context } }) => { +export const revalidateDelete: CollectionAfterDeleteHook = async ({ + doc, + req: { context }, +}) => { if (!context.disableRevalidate) { const path = `/posts/${doc?.slug}` - - revalidatePath(path) - revalidateTag('posts-sitemap') + try { + const { revalidatePath, revalidateTag } = await import('next/cache') + revalidatePath(path) + revalidateTag('posts-sitemap') + } catch {} } return doc diff --git a/apps/cms/src/collections/Posts/index.ts b/apps/cms/src/collections/Posts/index.ts index 402a323a6aa..7c20c798867 100644 --- a/apps/cms/src/collections/Posts/index.ts +++ b/apps/cms/src/collections/Posts/index.ts @@ -9,16 +9,16 @@ import { lexicalEditor, } from '@payloadcms/richtext-lexical' -import { isAnyone } from '@/access/isAnyone' -import { isAuthenticated } from '@/access/isAuthenticated' +import { isAnyone } from '../../access/isAnyone.ts' +import { isAuthenticated } from '../../access/isAuthenticated.ts' -import { Banner } from '@/blocks/Banner/config' -import { Code } from '@/blocks/Code/config' -import { MediaBlock } from '@/blocks/MediaBlock/config' -import { Quote } from '@/blocks/Quote/config' -import { YouTube } from '@/blocks/YouTube/config' -import { populateAuthors } from './hooks/populateAuthors' -import { revalidateDelete, revalidatePost } from './hooks/revalidatePost' +import { Banner } from '../../blocks/Banner/config.ts' +import { Code } from '../../blocks/Code/config.ts' +import { MediaBlock } from '../../blocks/MediaBlock/config.ts' +import { Quote } from '../../blocks/Quote/config.ts' +import { YouTube } from '../../blocks/YouTube/config.ts' +import { populateAuthors } from './hooks/populateAuthors.ts' +import { revalidateDelete, revalidatePost } from './hooks/revalidatePost.ts' import { MetaDescriptionField, @@ -27,7 +27,7 @@ import { OverviewField, PreviewField, } from '@payloadcms/plugin-seo/fields' -import { slugField } from '@/fields/slug' +import { slugField } from '../../fields/slug/index.ts' const launchweekOptions = [ { label: '6', value: '6' }, diff --git a/apps/cms/src/collections/Tags.ts b/apps/cms/src/collections/Tags.ts index 07bad6cdffc..d21dd5a4728 100644 --- a/apps/cms/src/collections/Tags.ts +++ b/apps/cms/src/collections/Tags.ts @@ -1,4 +1,4 @@ -import { CollectionConfig } from 'payload' +import type { CollectionConfig } from 'payload' export const Tags: CollectionConfig = { slug: 'tags', diff --git a/apps/cms/src/collections/Users.ts b/apps/cms/src/collections/Users.ts index 0108529b13c..de665459f8e 100644 --- a/apps/cms/src/collections/Users.ts +++ b/apps/cms/src/collections/Users.ts @@ -1,6 +1,6 @@ import type { CollectionConfig } from 'payload' -import { isAdmin, isAdminFieldLevel } from '@/access/isAdmin' -import { isAdminOrSelf } from '@/access/isAdminOrSelf' +import { isAdmin, isAdminFieldLevel } from '../access/isAdmin.ts' +import { isAdminOrSelf } from '../access/isAdminOrSelf.ts' export const Users: CollectionConfig = { slug: 'users', diff --git a/apps/cms/src/components/Card/index.tsx b/apps/cms/src/components/Card/index.tsx index d9587ebbba5..72c44d27f72 100644 --- a/apps/cms/src/components/Card/index.tsx +++ b/apps/cms/src/components/Card/index.tsx @@ -35,7 +35,7 @@ export const Card: React.FC<{ 'border border-border rounded-lg overflow-hidden bg-card hover:cursor-pointer', className )} - ref={card.ref} + ref={card.ref as React.Ref} >
{!metaImage &&
No image
} @@ -48,15 +48,15 @@ export const Card: React.FC<{
{categories?.map((category, index) => { if (typeof category === 'object') { - const { title: titleFromCategory } = category + const { name: nameFromCategory } = category - const categoryTitle = titleFromCategory || 'Untitled category' + const categoryName = nameFromCategory || 'Untitled category' const isLast = index === categories.length - 1 return ( - {categoryTitle} + {categoryName} {!isLast && ,  } ) @@ -71,7 +71,11 @@ export const Card: React.FC<{ {titleToUse && (

- + } + > {titleToUse}

diff --git a/apps/cms/src/components/Link/index.tsx b/apps/cms/src/components/Link/index.tsx index 0cf220eec59..ed9f7be0b23 100644 --- a/apps/cms/src/components/Link/index.tsx +++ b/apps/cms/src/components/Link/index.tsx @@ -3,7 +3,7 @@ import { cn } from '@/utilities/ui' import Link from 'next/link' import React from 'react' -import type { Page, Post } from '@/payload-types' +import type { Post } from '@/payload-types' type CMSLinkType = { appearance?: 'inline' | ButtonProps['variant'] @@ -12,8 +12,8 @@ type CMSLinkType = { label?: string | null newTab?: boolean | null reference?: { - relationTo: 'pages' | 'posts' - value: Page | Post | string | number + relationTo: 'posts' + value: Post | string | number } | null size?: ButtonProps['size'] | null type?: 'custom' | 'reference' | null @@ -35,9 +35,7 @@ export const CMSLink: React.FC = (props) => { const href = type === 'reference' && typeof reference?.value === 'object' && reference.value.slug - ? `${reference?.relationTo !== 'pages' ? `/${reference?.relationTo}` : ''}/${ - reference.value.slug - }` + ? `/${reference.value.slug}` : url if (!href) return null diff --git a/apps/cms/src/components/PayloadRedirects/index.tsx b/apps/cms/src/components/PayloadRedirects/index.tsx deleted file mode 100644 index d09f188e7b5..00000000000 --- a/apps/cms/src/components/PayloadRedirects/index.tsx +++ /dev/null @@ -1,48 +0,0 @@ -import type React from 'react' -import type { Post } from '@/payload-types' - -import { getCachedDocument } from '@/utilities/getDocument' -import { getCachedRedirects } from '@/utilities/getRedirects' -import { notFound, redirect } from 'next/navigation' - -interface Props { - disableNotFound?: boolean - url: string -} - -/* This component helps us with SSR based dynamic redirects */ -export const PayloadRedirects: React.FC = async ({ disableNotFound, url }) => { - const redirects = await getCachedRedirects()() - - const redirectItem = redirects.find((redirect) => redirect.from === url) - - if (redirectItem) { - if (redirectItem.to?.url) { - redirect(redirectItem.to.url) - } - - let redirectUrl: string - - if (typeof redirectItem.to?.reference?.value === 'string') { - const collection = redirectItem.to?.reference?.relationTo - const id = redirectItem.to?.reference?.value - - const document = (await getCachedDocument(collection, id)()) as Post - redirectUrl = `${redirectItem.to?.reference?.relationTo !== 'pages' ? `/${redirectItem.to?.reference?.relationTo}` : ''}/${ - document?.slug - }` - } else { - redirectUrl = `${redirectItem.to?.reference?.relationTo !== 'pages' ? `/${redirectItem.to?.reference?.relationTo}` : ''}/${ - typeof redirectItem.to?.reference?.value === 'object' - ? redirectItem.to?.reference?.value?.slug - : '' - }` - } - - if (redirectUrl) redirect(redirectUrl) - } - - if (disableNotFound) return null - - notFound() -} diff --git a/apps/cms/src/components/RichText/index.tsx b/apps/cms/src/components/RichText/index.tsx index b38c2a250fc..727236737d4 100644 --- a/apps/cms/src/components/RichText/index.tsx +++ b/apps/cms/src/components/RichText/index.tsx @@ -15,13 +15,11 @@ import { CodeBlock, CodeBlockProps } from '@/blocks/Code/Component' import type { BannerBlock as BannerBlockProps, - CallToActionBlock as CTABlockProps, MediaBlock as MediaBlockProps, QuoteBlock as QuoteBlockProps, YouTubeBlock as YouTubeBlockProps, } from '@/payload-types' import { BannerBlock } from '@/blocks/Banner/Component' -import { CallToActionBlock } from '@/blocks/CallToAction/Component' import { QuoteBlock } from '@/blocks/Quote/Component' import { YouTubeBlock } from '@/blocks/YouTube/Component' import { cn } from '@/utilities/ui' @@ -29,12 +27,7 @@ import { cn } from '@/utilities/ui' type NodeTypes = | DefaultNodeTypes | SerializedBlockNode< - | CTABlockProps - | MediaBlockProps - | BannerBlockProps - | CodeBlockProps - | QuoteBlockProps - | YouTubeBlockProps + MediaBlockProps | BannerBlockProps | CodeBlockProps | QuoteBlockProps | YouTubeBlockProps > const internalDocToHref = ({ linkNode }: { linkNode: SerializedLinkNode }) => { @@ -62,7 +55,6 @@ const jsxConverters: JSXConvertersFunction = ({ defaultConverters }) /> ), code: ({ node }) => , - cta: ({ node }) => , quote: ({ node }) => , youtube: ({ node }) => , }, diff --git a/apps/cms/src/fields/link.ts b/apps/cms/src/fields/link.ts index 2834e257aff..5b6b1a185b7 100644 --- a/apps/cms/src/fields/link.ts +++ b/apps/cms/src/fields/link.ts @@ -1,6 +1,6 @@ import type { Field, GroupField } from 'payload' -import deepMerge from '@/utilities/deepMerge' +import deepMerge from '../utilities/deepMerge.ts' export type LinkAppearances = 'default' | 'outline' @@ -75,7 +75,7 @@ export const link: LinkType = ({ appearances, disableLabel = false, overrides = condition: (_, siblingData) => siblingData?.type === 'reference', }, label: 'Document to link to', - relationTo: ['pages', 'posts'], + relationTo: ['posts'], required: true, }, { diff --git a/apps/cms/src/fields/linkGroup.ts b/apps/cms/src/fields/linkGroup.ts index c86fa33cf56..72e58a0cfae 100644 --- a/apps/cms/src/fields/linkGroup.ts +++ b/apps/cms/src/fields/linkGroup.ts @@ -1,9 +1,9 @@ import type { ArrayField, Field } from 'payload' -import type { LinkAppearances } from './link' +import type { LinkAppearances } from './link.ts' -import deepMerge from '@/utilities/deepMerge' -import { link } from './link' +import deepMerge from '../utilities/deepMerge.ts' +import { link } from './link.ts' type LinkGroupType = (options?: { appearances?: LinkAppearances[] | false diff --git a/apps/cms/src/fields/slug/index.ts b/apps/cms/src/fields/slug/index.ts index 5892ee5debb..9cbdfda8181 100644 --- a/apps/cms/src/fields/slug/index.ts +++ b/apps/cms/src/fields/slug/index.ts @@ -1,6 +1,6 @@ import type { CheckboxField, TextField } from 'payload' -import { formatSlugHook } from './formatSlug' +import { formatSlugHook } from './formatSlug.ts' type Overrides = { slugOverrides?: Partial diff --git a/apps/cms/src/migrations/20250529_103319.ts b/apps/cms/src/migrations/20250529_103319.ts deleted file mode 100644 index aee3c5f37a5..00000000000 --- a/apps/cms/src/migrations/20250529_103319.ts +++ /dev/null @@ -1,1136 +0,0 @@ -import { MigrateUpArgs, MigrateDownArgs, sql } from '@payloadcms/db-postgres' - -export async function up({ db }: MigrateUpArgs): Promise { - await db.execute(sql` - CREATE TYPE "cms-payload"."enum_customers_industry" AS ENUM('healthcare', 'fintech', 'ecommerce', 'education', 'gaming', 'media', 'real-estate', 'saas', 'social', 'analytics', 'ai', 'developer-tools'); - CREATE TYPE "cms-payload"."enum_customers_supabase_products" AS ENUM('database', 'auth', 'storage', 'realtime', 'functions', 'vector'); - CREATE TYPE "cms-payload"."enum_customers_company_size" AS ENUM('startup', 'enterprise', 'indie_dev'); - CREATE TYPE "cms-payload"."enum_customers_region" AS ENUM('Asia', 'Europe', 'North America', 'South America', 'Africa', 'Oceania'); - CREATE TYPE "cms-payload"."enum_customers_status" AS ENUM('draft', 'published'); - CREATE TYPE "cms-payload"."enum__customers_v_version_industry" AS ENUM('healthcare', 'fintech', 'ecommerce', 'education', 'gaming', 'media', 'real-estate', 'saas', 'social', 'analytics', 'ai', 'developer-tools'); - CREATE TYPE "cms-payload"."enum__customers_v_version_supabase_products" AS ENUM('database', 'auth', 'storage', 'realtime', 'functions', 'vector'); - CREATE TYPE "cms-payload"."enum__customers_v_version_company_size" AS ENUM('startup', 'enterprise', 'indie_dev'); - CREATE TYPE "cms-payload"."enum__customers_v_version_region" AS ENUM('Asia', 'Europe', 'North America', 'South America', 'Africa', 'Oceania'); - CREATE TYPE "cms-payload"."enum__customers_v_version_status" AS ENUM('draft', 'published'); - CREATE TYPE "cms-payload"."enum_events_type" AS ENUM('conference', 'hackathon', 'launch-week', 'meetup', 'office-hours', 'talk', 'webinar', 'workshop', 'other'); - CREATE TYPE "cms-payload"."enum_events_timezone" AS ENUM('Africa/Abidjan', 'Africa/Accra', 'Africa/Addis_Ababa', 'Africa/Algiers', 'Africa/Asmara', 'Africa/Bamako', 'Africa/Bangui', 'Africa/Banjul', 'Africa/Bissau', 'Africa/Blantyre', 'Africa/Brazzaville', 'Africa/Bujumbura', 'Africa/Cairo', 'Africa/Casablanca', 'Africa/Ceuta', 'Africa/Conakry', 'Africa/Dakar', 'Africa/Dar_es_Salaam', 'Africa/Djibouti', 'Africa/Douala', 'Africa/El_Aaiun', 'Africa/Freetown', 'Africa/Gaborone', 'Africa/Harare', 'Africa/Johannesburg', 'Africa/Juba', 'Africa/Kampala', 'Africa/Khartoum', 'Africa/Kigali', 'Africa/Kinshasa', 'Africa/Lagos', 'Africa/Libreville', 'Africa/Lome', 'Africa/Luanda', 'Africa/Lubumbashi', 'Africa/Lusaka', 'Africa/Malabo', 'Africa/Maputo', 'Africa/Maseru', 'Africa/Mbabane', 'Africa/Mogadishu', 'Africa/Monrovia', 'Africa/Nairobi', 'Africa/Ndjamena', 'Africa/Niamey', 'Africa/Nouakchott', 'Africa/Ouagadougou', 'Africa/Porto-Novo', 'Africa/Sao_Tome', 'Africa/Tripoli', 'Africa/Tunis', 'Africa/Windhoek', 'America/Adak', 'America/Anchorage', 'America/Anguilla', 'America/Antigua', 'America/Araguaina', 'America/Argentina/Buenos_Aires', 'America/Argentina/Catamarca', 'America/Argentina/Cordoba', 'America/Argentina/Jujuy', 'America/Argentina/La_Rioja', 'America/Argentina/Mendoza', 'America/Argentina/Rio_Gallegos', 'America/Argentina/Salta', 'America/Argentina/San_Juan', 'America/Argentina/San_Luis', 'America/Argentina/Tucuman', 'America/Argentina/Ushuaia', 'America/Aruba', 'America/Asuncion', 'America/Atikokan', 'America/Bahia', 'America/Bahia_Banderas', 'America/Barbados', 'America/Belem', 'America/Belize', 'America/Blanc-Sablon', 'America/Boa_Vista', 'America/Bogota', 'America/Boise', 'America/Cambridge_Bay', 'America/Campo_Grande', 'America/Cancun', 'America/Caracas', 'America/Cayenne', 'America/Cayman', 'America/Chicago', 'America/Chihuahua', 'America/Costa_Rica', 'America/Creston', 'America/Cuiaba', 'America/Curacao', 'America/Danmarkshavn', 'America/Dawson', 'America/Dawson_Creek', 'America/Denver', 'America/Detroit', 'America/Dominica', 'America/Edmonton', 'America/Eirunepe', 'America/El_Salvador', 'America/Fort_Nelson', 'America/Fortaleza', 'America/Glace_Bay', 'America/Godthab', 'America/Goose_Bay', 'America/Grand_Turk', 'America/Grenada', 'America/Guadeloupe', 'America/Guatemala', 'America/Guayaquil', 'America/Guyana', 'America/Halifax', 'America/Havana', 'America/Hermosillo', 'America/Indiana/Indianapolis', 'America/Indiana/Knox', 'America/Indiana/Marengo', 'America/Indiana/Petersburg', 'America/Indiana/Tell_City', 'America/Indiana/Vevay', 'America/Indiana/Vincennes', 'America/Indiana/Winamac', 'America/Inuvik', 'America/Iqaluit', 'America/Jamaica', 'America/Juneau', 'America/Kentucky/Louisville', 'America/Kentucky/Monticello', 'America/Kralendijk', 'America/La_Paz', 'America/Lima', 'America/Los_Angeles', 'America/Lower_Princes', 'America/Maceio', 'America/Managua', 'America/Manaus', 'America/Marigot', 'America/Martinique', 'America/Matamoros', 'America/Mazatlan', 'America/Menominee', 'America/Merida', 'America/Metlakatla', 'America/Mexico_City', 'America/Miquelon', 'America/Moncton', 'America/Monterrey', 'America/Montevideo', 'America/Montserrat', 'America/Nassau', 'America/New_York', 'America/Nipigon', 'America/Nome', 'America/Noronha', 'America/North_Dakota/Beulah', 'America/North_Dakota/Center', 'America/North_Dakota/New_Salem', 'America/Ojinaga', 'America/Panama', 'America/Pangnirtung', 'America/Paramaribo', 'America/Phoenix', 'America/Port-au-Prince', 'America/Port_of_Spain', 'America/Porto_Velho', 'America/Puerto_Rico', 'America/Punta_Arenas', 'America/Rainy_River', 'America/Rankin_Inlet', 'America/Recife', 'America/Regina', 'America/Resolute', 'America/Rio_Branco', 'America/Santarem', 'America/Santiago', 'America/Santo_Domingo', 'America/Sao_Paulo', 'America/Scoresbysund', 'America/Sitka', 'America/St_Barthelemy', 'America/St_Johns', 'America/St_Kitts', 'America/St_Lucia', 'America/St_Thomas', 'America/St_Vincent', 'America/Swift_Current', 'America/Tegucigalpa', 'America/Thule', 'America/Thunder_Bay', 'America/Tijuana', 'America/Toronto', 'America/Tortola', 'America/Vancouver', 'America/Whitehorse', 'America/Winnipeg', 'America/Yakutat', 'America/Yellowknife', 'Antarctica/Casey', 'Antarctica/Davis', 'Antarctica/DumontDUrville', 'Antarctica/Macquarie', 'Antarctica/Mawson', 'Antarctica/McMurdo', 'Antarctica/Palmer', 'Antarctica/Rothera', 'Antarctica/Syowa', 'Antarctica/Troll', 'Antarctica/Vostok', 'Arctic/Longyearbyen', 'Asia/Aden', 'Asia/Almaty', 'Asia/Amman', 'Asia/Anadyr', 'Asia/Aqtau', 'Asia/Aqtobe', 'Asia/Ashgabat', 'Asia/Atyrau', 'Asia/Baghdad', 'Asia/Bahrain', 'Asia/Baku', 'Asia/Bangkok', 'Asia/Barnaul', 'Asia/Beirut', 'Asia/Bishkek', 'Asia/Brunei', 'Asia/Chita', 'Asia/Choibalsan', 'Asia/Colombo', 'Asia/Damascus', 'Asia/Dhaka', 'Asia/Dili', 'Asia/Dubai', 'Asia/Dushanbe', 'Asia/Famagusta', 'Asia/Gaza', 'Asia/Hebron', 'Asia/Ho_Chi_Minh', 'Asia/Hong_Kong', 'Asia/Hovd', 'Asia/Irkutsk', 'Asia/Istanbul', 'Asia/Jakarta', 'Asia/Jayapura', 'Asia/Jerusalem', 'Asia/Kabul', 'Asia/Kamchatka', 'Asia/Karachi', 'Asia/Kathmandu', 'Asia/Khandyga', 'Asia/Kolkata', 'Asia/Krasnoyarsk', 'Asia/Kuala_Lumpur', 'Asia/Kuching', 'Asia/Kuwait', 'Asia/Macau', 'Asia/Magadan', 'Asia/Makassar', 'Asia/Manila', 'Asia/Muscat', 'Asia/Nicosia', 'Asia/Novokuznetsk', 'Asia/Novosibirsk', 'Asia/Omsk', 'Asia/Oral', 'Asia/Phnom_Penh', 'Asia/Pontianak', 'Asia/Pyongyang', 'Asia/Qatar', 'Asia/Qostanay', 'Asia/Qyzylorda', 'Asia/Riyadh', 'Asia/Sakhalin', 'Asia/Samarkand', 'Asia/Seoul', 'Asia/Shanghai', 'Asia/Singapore', 'Asia/Srednekolymsk', 'Asia/Taipei', 'Asia/Tashkent', 'Asia/Tbilisi', 'Asia/Tehran', 'Asia/Thimphu', 'Asia/Tokyo', 'Asia/Tomsk', 'Asia/Ulaanbaatar', 'Asia/Urumqi', 'Asia/Ust-Nera', 'Asia/Vientiane', 'Asia/Vladivostok', 'Asia/Yakutsk', 'Asia/Yangon', 'Asia/Yekaterinburg', 'Asia/Yerevan', 'Atlantic/Azores', 'Atlantic/Bermuda', 'Atlantic/Canary', 'Atlantic/Cape_Verde', 'Atlantic/Faroe', 'Atlantic/Madeira', 'Atlantic/Reykjavik', 'Atlantic/South_Georgia', 'Atlantic/St_Helena', 'Atlantic/Stanley', 'Australia/Adelaide', 'Australia/Brisbane', 'Australia/Broken_Hill', 'Australia/Currie', 'Australia/Darwin', 'Australia/Eucla', 'Australia/Hobart', 'Australia/Lindeman', 'Australia/Lord_Howe', 'Australia/Melbourne', 'Australia/Perth', 'Australia/Sydney', 'Europe/Amsterdam', 'Europe/Andorra', 'Europe/Astrakhan', 'Europe/Athens', 'Europe/Belgrade', 'Europe/Berlin', 'Europe/Bratislava', 'Europe/Brussels', 'Europe/Bucharest', 'Europe/Budapest', 'Europe/Busingen', 'Europe/Chisinau', 'Europe/Copenhagen', 'Europe/Dublin', 'Europe/Gibraltar', 'Europe/Guernsey', 'Europe/Helsinki', 'Europe/Isle_of_Man', 'Europe/Istanbul', 'Europe/Jersey', 'Europe/Kaliningrad', 'Europe/Kiev', 'Europe/Kirov', 'Europe/Lisbon', 'Europe/Ljubljana', 'Europe/London', 'Europe/Luxembourg', 'Europe/Madrid', 'Europe/Malta', 'Europe/Mariehamn', 'Europe/Minsk', 'Europe/Monaco', 'Europe/Moscow', 'Europe/Oslo', 'Europe/Paris', 'Europe/Podgorica', 'Europe/Prague', 'Europe/Riga', 'Europe/Rome', 'Europe/Samara', 'Europe/San_Marino', 'Europe/Sarajevo', 'Europe/Saratov', 'Europe/Simferopol', 'Europe/Skopje', 'Europe/Sofia', 'Europe/Stockholm', 'Europe/Tallinn', 'Europe/Tirane', 'Europe/Ulyanovsk', 'Europe/Uzhgorod', 'Europe/Vaduz', 'Europe/Vatican', 'Europe/Vienna', 'Europe/Vilnius', 'Europe/Volgograd', 'Europe/Warsaw', 'Europe/Zagreb', 'Europe/Zaporozhye', 'Europe/Zurich', 'Indian/Antananarivo', 'Indian/Chagos', 'Indian/Christmas', 'Indian/Cocos', 'Indian/Comoro', 'Indian/Kerguelen', 'Indian/Mahe', 'Indian/Maldives', 'Indian/Mauritius', 'Indian/Mayotte', 'Indian/Reunion', 'Pacific/Apia', 'Pacific/Auckland', 'Pacific/Bougainville', 'Pacific/Chatham', 'Pacific/Chuuk', 'Pacific/Easter', 'Pacific/Efate', 'Pacific/Enderbury', 'Pacific/Fakaofo', 'Pacific/Fiji', 'Pacific/Funafuti', 'Pacific/Galapagos', 'Pacific/Gambier', 'Pacific/Guadalcanal', 'Pacific/Guam', 'Pacific/Honolulu', 'Pacific/Kiritimati', 'Pacific/Kosrae', 'Pacific/Kwajalein', 'Pacific/Majuro', 'Pacific/Marquesas', 'Pacific/Midway', 'Pacific/Nauru', 'Pacific/Niue', 'Pacific/Norfolk', 'Pacific/Noumea', 'Pacific/Pago_Pago', 'Pacific/Palau', 'Pacific/Pitcairn', 'Pacific/Pohnpei', 'Pacific/Port_Moresby', 'Pacific/Rarotonga', 'Pacific/Saipan', 'Pacific/Tahiti', 'Pacific/Tarawa', 'Pacific/Tongatapu', 'Pacific/Wake', 'Pacific/Wallis', 'UTC'); - CREATE TYPE "cms-payload"."enum_events_link_target" AS ENUM('_self', '_blank'); - CREATE TYPE "cms-payload"."enum_events_main_cta_target" AS ENUM('_self', '_blank'); - CREATE TYPE "cms-payload"."enum_events_status" AS ENUM('draft', 'published'); - CREATE TYPE "cms-payload"."enum__events_v_version_type" AS ENUM('conference', 'hackathon', 'launch-week', 'meetup', 'office-hours', 'talk', 'webinar', 'workshop', 'other'); - CREATE TYPE "cms-payload"."enum__events_v_version_timezone" AS ENUM('Africa/Abidjan', 'Africa/Accra', 'Africa/Addis_Ababa', 'Africa/Algiers', 'Africa/Asmara', 'Africa/Bamako', 'Africa/Bangui', 'Africa/Banjul', 'Africa/Bissau', 'Africa/Blantyre', 'Africa/Brazzaville', 'Africa/Bujumbura', 'Africa/Cairo', 'Africa/Casablanca', 'Africa/Ceuta', 'Africa/Conakry', 'Africa/Dakar', 'Africa/Dar_es_Salaam', 'Africa/Djibouti', 'Africa/Douala', 'Africa/El_Aaiun', 'Africa/Freetown', 'Africa/Gaborone', 'Africa/Harare', 'Africa/Johannesburg', 'Africa/Juba', 'Africa/Kampala', 'Africa/Khartoum', 'Africa/Kigali', 'Africa/Kinshasa', 'Africa/Lagos', 'Africa/Libreville', 'Africa/Lome', 'Africa/Luanda', 'Africa/Lubumbashi', 'Africa/Lusaka', 'Africa/Malabo', 'Africa/Maputo', 'Africa/Maseru', 'Africa/Mbabane', 'Africa/Mogadishu', 'Africa/Monrovia', 'Africa/Nairobi', 'Africa/Ndjamena', 'Africa/Niamey', 'Africa/Nouakchott', 'Africa/Ouagadougou', 'Africa/Porto-Novo', 'Africa/Sao_Tome', 'Africa/Tripoli', 'Africa/Tunis', 'Africa/Windhoek', 'America/Adak', 'America/Anchorage', 'America/Anguilla', 'America/Antigua', 'America/Araguaina', 'America/Argentina/Buenos_Aires', 'America/Argentina/Catamarca', 'America/Argentina/Cordoba', 'America/Argentina/Jujuy', 'America/Argentina/La_Rioja', 'America/Argentina/Mendoza', 'America/Argentina/Rio_Gallegos', 'America/Argentina/Salta', 'America/Argentina/San_Juan', 'America/Argentina/San_Luis', 'America/Argentina/Tucuman', 'America/Argentina/Ushuaia', 'America/Aruba', 'America/Asuncion', 'America/Atikokan', 'America/Bahia', 'America/Bahia_Banderas', 'America/Barbados', 'America/Belem', 'America/Belize', 'America/Blanc-Sablon', 'America/Boa_Vista', 'America/Bogota', 'America/Boise', 'America/Cambridge_Bay', 'America/Campo_Grande', 'America/Cancun', 'America/Caracas', 'America/Cayenne', 'America/Cayman', 'America/Chicago', 'America/Chihuahua', 'America/Costa_Rica', 'America/Creston', 'America/Cuiaba', 'America/Curacao', 'America/Danmarkshavn', 'America/Dawson', 'America/Dawson_Creek', 'America/Denver', 'America/Detroit', 'America/Dominica', 'America/Edmonton', 'America/Eirunepe', 'America/El_Salvador', 'America/Fort_Nelson', 'America/Fortaleza', 'America/Glace_Bay', 'America/Godthab', 'America/Goose_Bay', 'America/Grand_Turk', 'America/Grenada', 'America/Guadeloupe', 'America/Guatemala', 'America/Guayaquil', 'America/Guyana', 'America/Halifax', 'America/Havana', 'America/Hermosillo', 'America/Indiana/Indianapolis', 'America/Indiana/Knox', 'America/Indiana/Marengo', 'America/Indiana/Petersburg', 'America/Indiana/Tell_City', 'America/Indiana/Vevay', 'America/Indiana/Vincennes', 'America/Indiana/Winamac', 'America/Inuvik', 'America/Iqaluit', 'America/Jamaica', 'America/Juneau', 'America/Kentucky/Louisville', 'America/Kentucky/Monticello', 'America/Kralendijk', 'America/La_Paz', 'America/Lima', 'America/Los_Angeles', 'America/Lower_Princes', 'America/Maceio', 'America/Managua', 'America/Manaus', 'America/Marigot', 'America/Martinique', 'America/Matamoros', 'America/Mazatlan', 'America/Menominee', 'America/Merida', 'America/Metlakatla', 'America/Mexico_City', 'America/Miquelon', 'America/Moncton', 'America/Monterrey', 'America/Montevideo', 'America/Montserrat', 'America/Nassau', 'America/New_York', 'America/Nipigon', 'America/Nome', 'America/Noronha', 'America/North_Dakota/Beulah', 'America/North_Dakota/Center', 'America/North_Dakota/New_Salem', 'America/Ojinaga', 'America/Panama', 'America/Pangnirtung', 'America/Paramaribo', 'America/Phoenix', 'America/Port-au-Prince', 'America/Port_of_Spain', 'America/Porto_Velho', 'America/Puerto_Rico', 'America/Punta_Arenas', 'America/Rainy_River', 'America/Rankin_Inlet', 'America/Recife', 'America/Regina', 'America/Resolute', 'America/Rio_Branco', 'America/Santarem', 'America/Santiago', 'America/Santo_Domingo', 'America/Sao_Paulo', 'America/Scoresbysund', 'America/Sitka', 'America/St_Barthelemy', 'America/St_Johns', 'America/St_Kitts', 'America/St_Lucia', 'America/St_Thomas', 'America/St_Vincent', 'America/Swift_Current', 'America/Tegucigalpa', 'America/Thule', 'America/Thunder_Bay', 'America/Tijuana', 'America/Toronto', 'America/Tortola', 'America/Vancouver', 'America/Whitehorse', 'America/Winnipeg', 'America/Yakutat', 'America/Yellowknife', 'Antarctica/Casey', 'Antarctica/Davis', 'Antarctica/DumontDUrville', 'Antarctica/Macquarie', 'Antarctica/Mawson', 'Antarctica/McMurdo', 'Antarctica/Palmer', 'Antarctica/Rothera', 'Antarctica/Syowa', 'Antarctica/Troll', 'Antarctica/Vostok', 'Arctic/Longyearbyen', 'Asia/Aden', 'Asia/Almaty', 'Asia/Amman', 'Asia/Anadyr', 'Asia/Aqtau', 'Asia/Aqtobe', 'Asia/Ashgabat', 'Asia/Atyrau', 'Asia/Baghdad', 'Asia/Bahrain', 'Asia/Baku', 'Asia/Bangkok', 'Asia/Barnaul', 'Asia/Beirut', 'Asia/Bishkek', 'Asia/Brunei', 'Asia/Chita', 'Asia/Choibalsan', 'Asia/Colombo', 'Asia/Damascus', 'Asia/Dhaka', 'Asia/Dili', 'Asia/Dubai', 'Asia/Dushanbe', 'Asia/Famagusta', 'Asia/Gaza', 'Asia/Hebron', 'Asia/Ho_Chi_Minh', 'Asia/Hong_Kong', 'Asia/Hovd', 'Asia/Irkutsk', 'Asia/Istanbul', 'Asia/Jakarta', 'Asia/Jayapura', 'Asia/Jerusalem', 'Asia/Kabul', 'Asia/Kamchatka', 'Asia/Karachi', 'Asia/Kathmandu', 'Asia/Khandyga', 'Asia/Kolkata', 'Asia/Krasnoyarsk', 'Asia/Kuala_Lumpur', 'Asia/Kuching', 'Asia/Kuwait', 'Asia/Macau', 'Asia/Magadan', 'Asia/Makassar', 'Asia/Manila', 'Asia/Muscat', 'Asia/Nicosia', 'Asia/Novokuznetsk', 'Asia/Novosibirsk', 'Asia/Omsk', 'Asia/Oral', 'Asia/Phnom_Penh', 'Asia/Pontianak', 'Asia/Pyongyang', 'Asia/Qatar', 'Asia/Qostanay', 'Asia/Qyzylorda', 'Asia/Riyadh', 'Asia/Sakhalin', 'Asia/Samarkand', 'Asia/Seoul', 'Asia/Shanghai', 'Asia/Singapore', 'Asia/Srednekolymsk', 'Asia/Taipei', 'Asia/Tashkent', 'Asia/Tbilisi', 'Asia/Tehran', 'Asia/Thimphu', 'Asia/Tokyo', 'Asia/Tomsk', 'Asia/Ulaanbaatar', 'Asia/Urumqi', 'Asia/Ust-Nera', 'Asia/Vientiane', 'Asia/Vladivostok', 'Asia/Yakutsk', 'Asia/Yangon', 'Asia/Yekaterinburg', 'Asia/Yerevan', 'Atlantic/Azores', 'Atlantic/Bermuda', 'Atlantic/Canary', 'Atlantic/Cape_Verde', 'Atlantic/Faroe', 'Atlantic/Madeira', 'Atlantic/Reykjavik', 'Atlantic/South_Georgia', 'Atlantic/St_Helena', 'Atlantic/Stanley', 'Australia/Adelaide', 'Australia/Brisbane', 'Australia/Broken_Hill', 'Australia/Currie', 'Australia/Darwin', 'Australia/Eucla', 'Australia/Hobart', 'Australia/Lindeman', 'Australia/Lord_Howe', 'Australia/Melbourne', 'Australia/Perth', 'Australia/Sydney', 'Europe/Amsterdam', 'Europe/Andorra', 'Europe/Astrakhan', 'Europe/Athens', 'Europe/Belgrade', 'Europe/Berlin', 'Europe/Bratislava', 'Europe/Brussels', 'Europe/Bucharest', 'Europe/Budapest', 'Europe/Busingen', 'Europe/Chisinau', 'Europe/Copenhagen', 'Europe/Dublin', 'Europe/Gibraltar', 'Europe/Guernsey', 'Europe/Helsinki', 'Europe/Isle_of_Man', 'Europe/Istanbul', 'Europe/Jersey', 'Europe/Kaliningrad', 'Europe/Kiev', 'Europe/Kirov', 'Europe/Lisbon', 'Europe/Ljubljana', 'Europe/London', 'Europe/Luxembourg', 'Europe/Madrid', 'Europe/Malta', 'Europe/Mariehamn', 'Europe/Minsk', 'Europe/Monaco', 'Europe/Moscow', 'Europe/Oslo', 'Europe/Paris', 'Europe/Podgorica', 'Europe/Prague', 'Europe/Riga', 'Europe/Rome', 'Europe/Samara', 'Europe/San_Marino', 'Europe/Sarajevo', 'Europe/Saratov', 'Europe/Simferopol', 'Europe/Skopje', 'Europe/Sofia', 'Europe/Stockholm', 'Europe/Tallinn', 'Europe/Tirane', 'Europe/Ulyanovsk', 'Europe/Uzhgorod', 'Europe/Vaduz', 'Europe/Vatican', 'Europe/Vienna', 'Europe/Vilnius', 'Europe/Volgograd', 'Europe/Warsaw', 'Europe/Zagreb', 'Europe/Zaporozhye', 'Europe/Zurich', 'Indian/Antananarivo', 'Indian/Chagos', 'Indian/Christmas', 'Indian/Cocos', 'Indian/Comoro', 'Indian/Kerguelen', 'Indian/Mahe', 'Indian/Maldives', 'Indian/Mauritius', 'Indian/Mayotte', 'Indian/Reunion', 'Pacific/Apia', 'Pacific/Auckland', 'Pacific/Bougainville', 'Pacific/Chatham', 'Pacific/Chuuk', 'Pacific/Easter', 'Pacific/Efate', 'Pacific/Enderbury', 'Pacific/Fakaofo', 'Pacific/Fiji', 'Pacific/Funafuti', 'Pacific/Galapagos', 'Pacific/Gambier', 'Pacific/Guadalcanal', 'Pacific/Guam', 'Pacific/Honolulu', 'Pacific/Kiritimati', 'Pacific/Kosrae', 'Pacific/Kwajalein', 'Pacific/Majuro', 'Pacific/Marquesas', 'Pacific/Midway', 'Pacific/Nauru', 'Pacific/Niue', 'Pacific/Norfolk', 'Pacific/Noumea', 'Pacific/Pago_Pago', 'Pacific/Palau', 'Pacific/Pitcairn', 'Pacific/Pohnpei', 'Pacific/Port_Moresby', 'Pacific/Rarotonga', 'Pacific/Saipan', 'Pacific/Tahiti', 'Pacific/Tarawa', 'Pacific/Tongatapu', 'Pacific/Wake', 'Pacific/Wallis', 'UTC'); - CREATE TYPE "cms-payload"."enum__events_v_version_link_target" AS ENUM('_self', '_blank'); - CREATE TYPE "cms-payload"."enum__events_v_version_main_cta_target" AS ENUM('_self', '_blank'); - CREATE TYPE "cms-payload"."enum__events_v_version_status" AS ENUM('draft', 'published'); - CREATE TYPE "cms-payload"."enum_posts_launchweek" AS ENUM('6', '7', '8', 'x', 'ga', '12', '13', '14', '15'); - CREATE TYPE "cms-payload"."enum_posts_status" AS ENUM('draft', 'published'); - CREATE TYPE "cms-payload"."enum__posts_v_version_launchweek" AS ENUM('6', '7', '8', 'x', 'ga', '12', '13', '14', '15'); - CREATE TYPE "cms-payload"."enum__posts_v_version_status" AS ENUM('draft', 'published'); - CREATE TYPE "cms-payload"."enum_users_roles" AS ENUM('admin', 'editor'); - CREATE TYPE "cms-payload"."enum_payload_jobs_log_task_slug" AS ENUM('inline', 'schedulePublish'); - CREATE TYPE "cms-payload"."enum_payload_jobs_log_state" AS ENUM('failed', 'succeeded'); - CREATE TYPE "cms-payload"."enum_payload_jobs_task_slug" AS ENUM('inline', 'schedulePublish'); - CREATE TABLE IF NOT EXISTS "cms-payload"."authors" ( - "id" serial PRIMARY KEY NOT NULL, - "author" varchar NOT NULL, - "author_id" varchar, - "position" varchar, - "author_url" varchar, - "author_image_url_id" integer, - "username" varchar, - "updated_at" timestamp(3) with time zone DEFAULT now() NOT NULL, - "created_at" timestamp(3) with time zone DEFAULT now() NOT NULL - ); - - CREATE TABLE IF NOT EXISTS "cms-payload"."categories_breadcrumbs" ( - "_order" integer NOT NULL, - "_parent_id" integer NOT NULL, - "id" varchar PRIMARY KEY NOT NULL, - "doc_id" integer, - "url" varchar, - "label" varchar - ); - - CREATE TABLE IF NOT EXISTS "cms-payload"."categories" ( - "id" serial PRIMARY KEY NOT NULL, - "name" varchar NOT NULL, - "parent_id" integer, - "updated_at" timestamp(3) with time zone DEFAULT now() NOT NULL, - "created_at" timestamp(3) with time zone DEFAULT now() NOT NULL - ); - - CREATE TABLE IF NOT EXISTS "cms-payload"."customers_stats" ( - "_order" integer NOT NULL, - "_parent_id" integer NOT NULL, - "id" varchar PRIMARY KEY NOT NULL, - "stat" varchar, - "label" varchar - ); - - CREATE TABLE IF NOT EXISTS "cms-payload"."customers_misc" ( - "_order" integer NOT NULL, - "_parent_id" integer NOT NULL, - "id" varchar PRIMARY KEY NOT NULL, - "label" varchar, - "text" varchar - ); - - CREATE TABLE IF NOT EXISTS "cms-payload"."customers_industry" ( - "order" integer NOT NULL, - "parent_id" integer NOT NULL, - "value" "cms-payload"."enum_customers_industry", - "id" serial PRIMARY KEY NOT NULL - ); - - CREATE TABLE IF NOT EXISTS "cms-payload"."customers_supabase_products" ( - "order" integer NOT NULL, - "parent_id" integer NOT NULL, - "value" "cms-payload"."enum_customers_supabase_products", - "id" serial PRIMARY KEY NOT NULL - ); - - CREATE TABLE IF NOT EXISTS "cms-payload"."customers" ( - "id" serial PRIMARY KEY NOT NULL, - "name" varchar, - "title" varchar, - "slug" varchar, - "slug_lock" boolean DEFAULT true, - "content" jsonb, - "description" varchar, - "about" varchar, - "company_url" varchar, - "company_size" "cms-payload"."enum_customers_company_size", - "region" "cms-payload"."enum_customers_region", - "meta_title" varchar, - "meta_image_id" integer, - "meta_description" varchar, - "published_at" timestamp(3) with time zone, - "logo_id" integer, - "logo_inverse_id" integer, - "updated_at" timestamp(3) with time zone DEFAULT now() NOT NULL, - "created_at" timestamp(3) with time zone DEFAULT now() NOT NULL, - "_status" "cms-payload"."enum_customers_status" DEFAULT 'draft' - ); - - CREATE TABLE IF NOT EXISTS "cms-payload"."_customers_v_version_stats" ( - "_order" integer NOT NULL, - "_parent_id" integer NOT NULL, - "id" serial PRIMARY KEY NOT NULL, - "stat" varchar, - "label" varchar, - "_uuid" varchar - ); - - CREATE TABLE IF NOT EXISTS "cms-payload"."_customers_v_version_misc" ( - "_order" integer NOT NULL, - "_parent_id" integer NOT NULL, - "id" serial PRIMARY KEY NOT NULL, - "label" varchar, - "text" varchar, - "_uuid" varchar - ); - - CREATE TABLE IF NOT EXISTS "cms-payload"."_customers_v_version_industry" ( - "order" integer NOT NULL, - "parent_id" integer NOT NULL, - "value" "cms-payload"."enum__customers_v_version_industry", - "id" serial PRIMARY KEY NOT NULL - ); - - CREATE TABLE IF NOT EXISTS "cms-payload"."_customers_v_version_supabase_products" ( - "order" integer NOT NULL, - "parent_id" integer NOT NULL, - "value" "cms-payload"."enum__customers_v_version_supabase_products", - "id" serial PRIMARY KEY NOT NULL - ); - - CREATE TABLE IF NOT EXISTS "cms-payload"."_customers_v" ( - "id" serial PRIMARY KEY NOT NULL, - "parent_id" integer, - "version_name" varchar, - "version_title" varchar, - "version_slug" varchar, - "version_slug_lock" boolean DEFAULT true, - "version_content" jsonb, - "version_description" varchar, - "version_about" varchar, - "version_company_url" varchar, - "version_company_size" "cms-payload"."enum__customers_v_version_company_size", - "version_region" "cms-payload"."enum__customers_v_version_region", - "version_meta_title" varchar, - "version_meta_image_id" integer, - "version_meta_description" varchar, - "version_published_at" timestamp(3) with time zone, - "version_logo_id" integer, - "version_logo_inverse_id" integer, - "version_updated_at" timestamp(3) with time zone, - "version_created_at" timestamp(3) with time zone, - "version__status" "cms-payload"."enum__customers_v_version_status" DEFAULT 'draft', - "created_at" timestamp(3) with time zone DEFAULT now() NOT NULL, - "updated_at" timestamp(3) with time zone DEFAULT now() NOT NULL, - "latest" boolean, - "autosave" boolean - ); - - CREATE TABLE IF NOT EXISTS "cms-payload"."events_type" ( - "order" integer NOT NULL, - "parent_id" integer NOT NULL, - "value" "cms-payload"."enum_events_type", - "id" serial PRIMARY KEY NOT NULL - ); - - CREATE TABLE IF NOT EXISTS "cms-payload"."events" ( - "id" serial PRIMARY KEY NOT NULL, - "title" varchar, - "slug" varchar, - "slug_lock" boolean DEFAULT true, - "subtitle" varchar, - "content" jsonb, - "thumb_id" integer, - "image_id" integer, - "date" timestamp(3) with time zone, - "timezone" "cms-payload"."enum_events_timezone", - "show_end_date" boolean DEFAULT false, - "end_date" timestamp(3) with time zone, - "duration" varchar, - "on_demand" boolean DEFAULT false, - "disable_page_build" boolean DEFAULT false, - "link_href" varchar, - "link_target" "cms-payload"."enum_events_link_target" DEFAULT '_blank', - "main_cta_href" varchar, - "main_cta_target" "cms-payload"."enum_events_main_cta_target" DEFAULT '_blank', - "main_cta_label" varchar, - "main_cta_disabled" boolean DEFAULT false, - "main_cta_disabled_label" varchar, - "company_show_company" boolean DEFAULT false, - "company_name" varchar, - "company_website_url" varchar, - "company_logo_id" integer, - "company_logo_light_id" integer, - "participants_show_participants" boolean DEFAULT false, - "meta_title" varchar, - "meta_image_id" integer, - "meta_description" varchar, - "published_at" timestamp(3) with time zone, - "updated_at" timestamp(3) with time zone DEFAULT now() NOT NULL, - "created_at" timestamp(3) with time zone DEFAULT now() NOT NULL, - "_status" "cms-payload"."enum_events_status" DEFAULT 'draft' - ); - - CREATE TABLE IF NOT EXISTS "cms-payload"."events_rels" ( - "id" serial PRIMARY KEY NOT NULL, - "order" integer, - "parent_id" integer NOT NULL, - "path" varchar NOT NULL, - "authors_id" integer - ); - - CREATE TABLE IF NOT EXISTS "cms-payload"."_events_v_version_type" ( - "order" integer NOT NULL, - "parent_id" integer NOT NULL, - "value" "cms-payload"."enum__events_v_version_type", - "id" serial PRIMARY KEY NOT NULL - ); - - CREATE TABLE IF NOT EXISTS "cms-payload"."_events_v" ( - "id" serial PRIMARY KEY NOT NULL, - "parent_id" integer, - "version_title" varchar, - "version_slug" varchar, - "version_slug_lock" boolean DEFAULT true, - "version_subtitle" varchar, - "version_content" jsonb, - "version_thumb_id" integer, - "version_image_id" integer, - "version_date" timestamp(3) with time zone, - "version_timezone" "cms-payload"."enum__events_v_version_timezone", - "version_show_end_date" boolean DEFAULT false, - "version_end_date" timestamp(3) with time zone, - "version_duration" varchar, - "version_on_demand" boolean DEFAULT false, - "version_disable_page_build" boolean DEFAULT false, - "version_link_href" varchar, - "version_link_target" "cms-payload"."enum__events_v_version_link_target" DEFAULT '_blank', - "version_main_cta_href" varchar, - "version_main_cta_target" "cms-payload"."enum__events_v_version_main_cta_target" DEFAULT '_blank', - "version_main_cta_label" varchar, - "version_main_cta_disabled" boolean DEFAULT false, - "version_main_cta_disabled_label" varchar, - "version_company_show_company" boolean DEFAULT false, - "version_company_name" varchar, - "version_company_website_url" varchar, - "version_company_logo_id" integer, - "version_company_logo_light_id" integer, - "version_participants_show_participants" boolean DEFAULT false, - "version_meta_title" varchar, - "version_meta_image_id" integer, - "version_meta_description" varchar, - "version_published_at" timestamp(3) with time zone, - "version_updated_at" timestamp(3) with time zone, - "version_created_at" timestamp(3) with time zone, - "version__status" "cms-payload"."enum__events_v_version_status" DEFAULT 'draft', - "created_at" timestamp(3) with time zone DEFAULT now() NOT NULL, - "updated_at" timestamp(3) with time zone DEFAULT now() NOT NULL, - "latest" boolean, - "autosave" boolean - ); - - CREATE TABLE IF NOT EXISTS "cms-payload"."_events_v_rels" ( - "id" serial PRIMARY KEY NOT NULL, - "order" integer, - "parent_id" integer NOT NULL, - "path" varchar NOT NULL, - "authors_id" integer - ); - - CREATE TABLE IF NOT EXISTS "cms-payload"."media" ( - "id" serial PRIMARY KEY NOT NULL, - "alt" varchar, - "caption" jsonb, - "prefix" varchar DEFAULT 'media', - "updated_at" timestamp(3) with time zone DEFAULT now() NOT NULL, - "created_at" timestamp(3) with time zone DEFAULT now() NOT NULL, - "url" varchar, - "thumbnail_u_r_l" varchar, - "filename" varchar, - "mime_type" varchar, - "filesize" numeric, - "width" numeric, - "height" numeric, - "focal_x" numeric, - "focal_y" numeric, - "sizes_thumbnail_url" varchar, - "sizes_thumbnail_width" numeric, - "sizes_thumbnail_height" numeric, - "sizes_thumbnail_mime_type" varchar, - "sizes_thumbnail_filesize" numeric, - "sizes_thumbnail_filename" varchar, - "sizes_square_url" varchar, - "sizes_square_width" numeric, - "sizes_square_height" numeric, - "sizes_square_mime_type" varchar, - "sizes_square_filesize" numeric, - "sizes_square_filename" varchar, - "sizes_small_url" varchar, - "sizes_small_width" numeric, - "sizes_small_height" numeric, - "sizes_small_mime_type" varchar, - "sizes_small_filesize" numeric, - "sizes_small_filename" varchar, - "sizes_medium_url" varchar, - "sizes_medium_width" numeric, - "sizes_medium_height" numeric, - "sizes_medium_mime_type" varchar, - "sizes_medium_filesize" numeric, - "sizes_medium_filename" varchar, - "sizes_large_url" varchar, - "sizes_large_width" numeric, - "sizes_large_height" numeric, - "sizes_large_mime_type" varchar, - "sizes_large_filesize" numeric, - "sizes_large_filename" varchar, - "sizes_xlarge_url" varchar, - "sizes_xlarge_width" numeric, - "sizes_xlarge_height" numeric, - "sizes_xlarge_mime_type" varchar, - "sizes_xlarge_filesize" numeric, - "sizes_xlarge_filename" varchar, - "sizes_og_url" varchar, - "sizes_og_width" numeric, - "sizes_og_height" numeric, - "sizes_og_mime_type" varchar, - "sizes_og_filesize" numeric, - "sizes_og_filename" varchar - ); - - CREATE TABLE IF NOT EXISTS "cms-payload"."posts" ( - "id" serial PRIMARY KEY NOT NULL, - "title" varchar, - "slug" varchar, - "slug_lock" boolean DEFAULT true, - "content" jsonb, - "thumb_id" integer, - "image_id" integer, - "launchweek" "cms-payload"."enum_posts_launchweek", - "reading_time" numeric, - "date" timestamp(3) with time zone, - "toc_depth" numeric DEFAULT 2, - "description" varchar, - "meta_title" varchar, - "meta_image_id" integer, - "meta_description" varchar, - "published_at" timestamp(3) with time zone, - "updated_at" timestamp(3) with time zone DEFAULT now() NOT NULL, - "created_at" timestamp(3) with time zone DEFAULT now() NOT NULL, - "_status" "cms-payload"."enum_posts_status" DEFAULT 'draft' - ); - - CREATE TABLE IF NOT EXISTS "cms-payload"."posts_rels" ( - "id" serial PRIMARY KEY NOT NULL, - "order" integer, - "parent_id" integer NOT NULL, - "path" varchar NOT NULL, - "categories_id" integer, - "authors_id" integer, - "tags_id" integer - ); - - CREATE TABLE IF NOT EXISTS "cms-payload"."_posts_v" ( - "id" serial PRIMARY KEY NOT NULL, - "parent_id" integer, - "version_title" varchar, - "version_slug" varchar, - "version_slug_lock" boolean DEFAULT true, - "version_content" jsonb, - "version_thumb_id" integer, - "version_image_id" integer, - "version_launchweek" "cms-payload"."enum__posts_v_version_launchweek", - "version_reading_time" numeric, - "version_date" timestamp(3) with time zone, - "version_toc_depth" numeric DEFAULT 2, - "version_description" varchar, - "version_meta_title" varchar, - "version_meta_image_id" integer, - "version_meta_description" varchar, - "version_published_at" timestamp(3) with time zone, - "version_updated_at" timestamp(3) with time zone, - "version_created_at" timestamp(3) with time zone, - "version__status" "cms-payload"."enum__posts_v_version_status" DEFAULT 'draft', - "created_at" timestamp(3) with time zone DEFAULT now() NOT NULL, - "updated_at" timestamp(3) with time zone DEFAULT now() NOT NULL, - "latest" boolean, - "autosave" boolean - ); - - CREATE TABLE IF NOT EXISTS "cms-payload"."_posts_v_rels" ( - "id" serial PRIMARY KEY NOT NULL, - "order" integer, - "parent_id" integer NOT NULL, - "path" varchar NOT NULL, - "categories_id" integer, - "authors_id" integer, - "tags_id" integer - ); - - CREATE TABLE IF NOT EXISTS "cms-payload"."tags" ( - "id" serial PRIMARY KEY NOT NULL, - "name" varchar NOT NULL, - "updated_at" timestamp(3) with time zone DEFAULT now() NOT NULL, - "created_at" timestamp(3) with time zone DEFAULT now() NOT NULL - ); - - CREATE TABLE IF NOT EXISTS "cms-payload"."users_roles" ( - "order" integer NOT NULL, - "parent_id" integer NOT NULL, - "value" "cms-payload"."enum_users_roles", - "id" serial PRIMARY KEY NOT NULL - ); - - CREATE TABLE IF NOT EXISTS "cms-payload"."users" ( - "id" serial PRIMARY KEY NOT NULL, - "updated_at" timestamp(3) with time zone DEFAULT now() NOT NULL, - "created_at" timestamp(3) with time zone DEFAULT now() NOT NULL, - "email" varchar NOT NULL, - "reset_password_token" varchar, - "reset_password_expiration" timestamp(3) with time zone, - "salt" varchar, - "hash" varchar, - "login_attempts" numeric DEFAULT 0, - "lock_until" timestamp(3) with time zone - ); - - CREATE TABLE IF NOT EXISTS "cms-payload"."payload_jobs_log" ( - "_order" integer NOT NULL, - "_parent_id" integer NOT NULL, - "id" varchar PRIMARY KEY NOT NULL, - "executed_at" timestamp(3) with time zone NOT NULL, - "completed_at" timestamp(3) with time zone NOT NULL, - "task_slug" "cms-payload"."enum_payload_jobs_log_task_slug" NOT NULL, - "task_i_d" varchar NOT NULL, - "input" jsonb, - "output" jsonb, - "state" "cms-payload"."enum_payload_jobs_log_state" NOT NULL, - "error" jsonb - ); - - CREATE TABLE IF NOT EXISTS "cms-payload"."payload_jobs" ( - "id" serial PRIMARY KEY NOT NULL, - "input" jsonb, - "completed_at" timestamp(3) with time zone, - "total_tried" numeric DEFAULT 0, - "has_error" boolean DEFAULT false, - "error" jsonb, - "task_slug" "cms-payload"."enum_payload_jobs_task_slug", - "queue" varchar DEFAULT 'default', - "wait_until" timestamp(3) with time zone, - "processing" boolean DEFAULT false, - "updated_at" timestamp(3) with time zone DEFAULT now() NOT NULL, - "created_at" timestamp(3) with time zone DEFAULT now() NOT NULL - ); - - CREATE TABLE IF NOT EXISTS "cms-payload"."payload_locked_documents" ( - "id" serial PRIMARY KEY NOT NULL, - "global_slug" varchar, - "updated_at" timestamp(3) with time zone DEFAULT now() NOT NULL, - "created_at" timestamp(3) with time zone DEFAULT now() NOT NULL - ); - - CREATE TABLE IF NOT EXISTS "cms-payload"."payload_locked_documents_rels" ( - "id" serial PRIMARY KEY NOT NULL, - "order" integer, - "parent_id" integer NOT NULL, - "path" varchar NOT NULL, - "authors_id" integer, - "categories_id" integer, - "customers_id" integer, - "events_id" integer, - "media_id" integer, - "posts_id" integer, - "tags_id" integer, - "users_id" integer, - "payload_jobs_id" integer - ); - - CREATE TABLE IF NOT EXISTS "cms-payload"."payload_preferences" ( - "id" serial PRIMARY KEY NOT NULL, - "key" varchar, - "value" jsonb, - "updated_at" timestamp(3) with time zone DEFAULT now() NOT NULL, - "created_at" timestamp(3) with time zone DEFAULT now() NOT NULL - ); - - CREATE TABLE IF NOT EXISTS "cms-payload"."payload_preferences_rels" ( - "id" serial PRIMARY KEY NOT NULL, - "order" integer, - "parent_id" integer NOT NULL, - "path" varchar NOT NULL, - "users_id" integer - ); - - CREATE TABLE IF NOT EXISTS "cms-payload"."payload_migrations" ( - "id" serial PRIMARY KEY NOT NULL, - "name" varchar, - "batch" numeric, - "updated_at" timestamp(3) with time zone DEFAULT now() NOT NULL, - "created_at" timestamp(3) with time zone DEFAULT now() NOT NULL - ); - - DO $$ BEGIN - ALTER TABLE "cms-payload"."authors" ADD CONSTRAINT "authors_author_image_url_id_media_id_fk" FOREIGN KEY ("author_image_url_id") REFERENCES "cms-payload"."media"("id") ON DELETE set null ON UPDATE no action; - EXCEPTION - WHEN duplicate_object THEN null; - END $$; - - DO $$ BEGIN - ALTER TABLE "cms-payload"."categories_breadcrumbs" ADD CONSTRAINT "categories_breadcrumbs_doc_id_categories_id_fk" FOREIGN KEY ("doc_id") REFERENCES "cms-payload"."categories"("id") ON DELETE set null ON UPDATE no action; - EXCEPTION - WHEN duplicate_object THEN null; - END $$; - - DO $$ BEGIN - ALTER TABLE "cms-payload"."categories_breadcrumbs" ADD CONSTRAINT "categories_breadcrumbs_parent_id_fk" FOREIGN KEY ("_parent_id") REFERENCES "cms-payload"."categories"("id") ON DELETE cascade ON UPDATE no action; - EXCEPTION - WHEN duplicate_object THEN null; - END $$; - - DO $$ BEGIN - ALTER TABLE "cms-payload"."categories" ADD CONSTRAINT "categories_parent_id_categories_id_fk" FOREIGN KEY ("parent_id") REFERENCES "cms-payload"."categories"("id") ON DELETE set null ON UPDATE no action; - EXCEPTION - WHEN duplicate_object THEN null; - END $$; - - DO $$ BEGIN - ALTER TABLE "cms-payload"."customers_stats" ADD CONSTRAINT "customers_stats_parent_id_fk" FOREIGN KEY ("_parent_id") REFERENCES "cms-payload"."customers"("id") ON DELETE cascade ON UPDATE no action; - EXCEPTION - WHEN duplicate_object THEN null; - END $$; - - DO $$ BEGIN - ALTER TABLE "cms-payload"."customers_misc" ADD CONSTRAINT "customers_misc_parent_id_fk" FOREIGN KEY ("_parent_id") REFERENCES "cms-payload"."customers"("id") ON DELETE cascade ON UPDATE no action; - EXCEPTION - WHEN duplicate_object THEN null; - END $$; - - DO $$ BEGIN - ALTER TABLE "cms-payload"."customers_industry" ADD CONSTRAINT "customers_industry_parent_fk" FOREIGN KEY ("parent_id") REFERENCES "cms-payload"."customers"("id") ON DELETE cascade ON UPDATE no action; - EXCEPTION - WHEN duplicate_object THEN null; - END $$; - - DO $$ BEGIN - ALTER TABLE "cms-payload"."customers_supabase_products" ADD CONSTRAINT "customers_supabase_products_parent_fk" FOREIGN KEY ("parent_id") REFERENCES "cms-payload"."customers"("id") ON DELETE cascade ON UPDATE no action; - EXCEPTION - WHEN duplicate_object THEN null; - END $$; - - DO $$ BEGIN - ALTER TABLE "cms-payload"."customers" ADD CONSTRAINT "customers_meta_image_id_media_id_fk" FOREIGN KEY ("meta_image_id") REFERENCES "cms-payload"."media"("id") ON DELETE set null ON UPDATE no action; - EXCEPTION - WHEN duplicate_object THEN null; - END $$; - - DO $$ BEGIN - ALTER TABLE "cms-payload"."customers" ADD CONSTRAINT "customers_logo_id_media_id_fk" FOREIGN KEY ("logo_id") REFERENCES "cms-payload"."media"("id") ON DELETE set null ON UPDATE no action; - EXCEPTION - WHEN duplicate_object THEN null; - END $$; - - DO $$ BEGIN - ALTER TABLE "cms-payload"."customers" ADD CONSTRAINT "customers_logo_inverse_id_media_id_fk" FOREIGN KEY ("logo_inverse_id") REFERENCES "cms-payload"."media"("id") ON DELETE set null ON UPDATE no action; - EXCEPTION - WHEN duplicate_object THEN null; - END $$; - - DO $$ BEGIN - ALTER TABLE "cms-payload"."_customers_v_version_stats" ADD CONSTRAINT "_customers_v_version_stats_parent_id_fk" FOREIGN KEY ("_parent_id") REFERENCES "cms-payload"."_customers_v"("id") ON DELETE cascade ON UPDATE no action; - EXCEPTION - WHEN duplicate_object THEN null; - END $$; - - DO $$ BEGIN - ALTER TABLE "cms-payload"."_customers_v_version_misc" ADD CONSTRAINT "_customers_v_version_misc_parent_id_fk" FOREIGN KEY ("_parent_id") REFERENCES "cms-payload"."_customers_v"("id") ON DELETE cascade ON UPDATE no action; - EXCEPTION - WHEN duplicate_object THEN null; - END $$; - - DO $$ BEGIN - ALTER TABLE "cms-payload"."_customers_v_version_industry" ADD CONSTRAINT "_customers_v_version_industry_parent_fk" FOREIGN KEY ("parent_id") REFERENCES "cms-payload"."_customers_v"("id") ON DELETE cascade ON UPDATE no action; - EXCEPTION - WHEN duplicate_object THEN null; - END $$; - - DO $$ BEGIN - ALTER TABLE "cms-payload"."_customers_v_version_supabase_products" ADD CONSTRAINT "_customers_v_version_supabase_products_parent_fk" FOREIGN KEY ("parent_id") REFERENCES "cms-payload"."_customers_v"("id") ON DELETE cascade ON UPDATE no action; - EXCEPTION - WHEN duplicate_object THEN null; - END $$; - - DO $$ BEGIN - ALTER TABLE "cms-payload"."_customers_v" ADD CONSTRAINT "_customers_v_parent_id_customers_id_fk" FOREIGN KEY ("parent_id") REFERENCES "cms-payload"."customers"("id") ON DELETE set null ON UPDATE no action; - EXCEPTION - WHEN duplicate_object THEN null; - END $$; - - DO $$ BEGIN - ALTER TABLE "cms-payload"."_customers_v" ADD CONSTRAINT "_customers_v_version_meta_image_id_media_id_fk" FOREIGN KEY ("version_meta_image_id") REFERENCES "cms-payload"."media"("id") ON DELETE set null ON UPDATE no action; - EXCEPTION - WHEN duplicate_object THEN null; - END $$; - - DO $$ BEGIN - ALTER TABLE "cms-payload"."_customers_v" ADD CONSTRAINT "_customers_v_version_logo_id_media_id_fk" FOREIGN KEY ("version_logo_id") REFERENCES "cms-payload"."media"("id") ON DELETE set null ON UPDATE no action; - EXCEPTION - WHEN duplicate_object THEN null; - END $$; - - DO $$ BEGIN - ALTER TABLE "cms-payload"."_customers_v" ADD CONSTRAINT "_customers_v_version_logo_inverse_id_media_id_fk" FOREIGN KEY ("version_logo_inverse_id") REFERENCES "cms-payload"."media"("id") ON DELETE set null ON UPDATE no action; - EXCEPTION - WHEN duplicate_object THEN null; - END $$; - - DO $$ BEGIN - ALTER TABLE "cms-payload"."events_type" ADD CONSTRAINT "events_type_parent_fk" FOREIGN KEY ("parent_id") REFERENCES "cms-payload"."events"("id") ON DELETE cascade ON UPDATE no action; - EXCEPTION - WHEN duplicate_object THEN null; - END $$; - - DO $$ BEGIN - ALTER TABLE "cms-payload"."events" ADD CONSTRAINT "events_thumb_id_media_id_fk" FOREIGN KEY ("thumb_id") REFERENCES "cms-payload"."media"("id") ON DELETE set null ON UPDATE no action; - EXCEPTION - WHEN duplicate_object THEN null; - END $$; - - DO $$ BEGIN - ALTER TABLE "cms-payload"."events" ADD CONSTRAINT "events_image_id_media_id_fk" FOREIGN KEY ("image_id") REFERENCES "cms-payload"."media"("id") ON DELETE set null ON UPDATE no action; - EXCEPTION - WHEN duplicate_object THEN null; - END $$; - - DO $$ BEGIN - ALTER TABLE "cms-payload"."events" ADD CONSTRAINT "events_company_logo_id_media_id_fk" FOREIGN KEY ("company_logo_id") REFERENCES "cms-payload"."media"("id") ON DELETE set null ON UPDATE no action; - EXCEPTION - WHEN duplicate_object THEN null; - END $$; - - DO $$ BEGIN - ALTER TABLE "cms-payload"."events" ADD CONSTRAINT "events_company_logo_light_id_media_id_fk" FOREIGN KEY ("company_logo_light_id") REFERENCES "cms-payload"."media"("id") ON DELETE set null ON UPDATE no action; - EXCEPTION - WHEN duplicate_object THEN null; - END $$; - - DO $$ BEGIN - ALTER TABLE "cms-payload"."events" ADD CONSTRAINT "events_meta_image_id_media_id_fk" FOREIGN KEY ("meta_image_id") REFERENCES "cms-payload"."media"("id") ON DELETE set null ON UPDATE no action; - EXCEPTION - WHEN duplicate_object THEN null; - END $$; - - DO $$ BEGIN - ALTER TABLE "cms-payload"."events_rels" ADD CONSTRAINT "events_rels_parent_fk" FOREIGN KEY ("parent_id") REFERENCES "cms-payload"."events"("id") ON DELETE cascade ON UPDATE no action; - EXCEPTION - WHEN duplicate_object THEN null; - END $$; - - DO $$ BEGIN - ALTER TABLE "cms-payload"."events_rels" ADD CONSTRAINT "events_rels_authors_fk" FOREIGN KEY ("authors_id") REFERENCES "cms-payload"."authors"("id") ON DELETE cascade ON UPDATE no action; - EXCEPTION - WHEN duplicate_object THEN null; - END $$; - - DO $$ BEGIN - ALTER TABLE "cms-payload"."_events_v_version_type" ADD CONSTRAINT "_events_v_version_type_parent_fk" FOREIGN KEY ("parent_id") REFERENCES "cms-payload"."_events_v"("id") ON DELETE cascade ON UPDATE no action; - EXCEPTION - WHEN duplicate_object THEN null; - END $$; - - DO $$ BEGIN - ALTER TABLE "cms-payload"."_events_v" ADD CONSTRAINT "_events_v_parent_id_events_id_fk" FOREIGN KEY ("parent_id") REFERENCES "cms-payload"."events"("id") ON DELETE set null ON UPDATE no action; - EXCEPTION - WHEN duplicate_object THEN null; - END $$; - - DO $$ BEGIN - ALTER TABLE "cms-payload"."_events_v" ADD CONSTRAINT "_events_v_version_thumb_id_media_id_fk" FOREIGN KEY ("version_thumb_id") REFERENCES "cms-payload"."media"("id") ON DELETE set null ON UPDATE no action; - EXCEPTION - WHEN duplicate_object THEN null; - END $$; - - DO $$ BEGIN - ALTER TABLE "cms-payload"."_events_v" ADD CONSTRAINT "_events_v_version_image_id_media_id_fk" FOREIGN KEY ("version_image_id") REFERENCES "cms-payload"."media"("id") ON DELETE set null ON UPDATE no action; - EXCEPTION - WHEN duplicate_object THEN null; - END $$; - - DO $$ BEGIN - ALTER TABLE "cms-payload"."_events_v" ADD CONSTRAINT "_events_v_version_company_logo_id_media_id_fk" FOREIGN KEY ("version_company_logo_id") REFERENCES "cms-payload"."media"("id") ON DELETE set null ON UPDATE no action; - EXCEPTION - WHEN duplicate_object THEN null; - END $$; - - DO $$ BEGIN - ALTER TABLE "cms-payload"."_events_v" ADD CONSTRAINT "_events_v_version_company_logo_light_id_media_id_fk" FOREIGN KEY ("version_company_logo_light_id") REFERENCES "cms-payload"."media"("id") ON DELETE set null ON UPDATE no action; - EXCEPTION - WHEN duplicate_object THEN null; - END $$; - - DO $$ BEGIN - ALTER TABLE "cms-payload"."_events_v" ADD CONSTRAINT "_events_v_version_meta_image_id_media_id_fk" FOREIGN KEY ("version_meta_image_id") REFERENCES "cms-payload"."media"("id") ON DELETE set null ON UPDATE no action; - EXCEPTION - WHEN duplicate_object THEN null; - END $$; - - DO $$ BEGIN - ALTER TABLE "cms-payload"."_events_v_rels" ADD CONSTRAINT "_events_v_rels_parent_fk" FOREIGN KEY ("parent_id") REFERENCES "cms-payload"."_events_v"("id") ON DELETE cascade ON UPDATE no action; - EXCEPTION - WHEN duplicate_object THEN null; - END $$; - - DO $$ BEGIN - ALTER TABLE "cms-payload"."_events_v_rels" ADD CONSTRAINT "_events_v_rels_authors_fk" FOREIGN KEY ("authors_id") REFERENCES "cms-payload"."authors"("id") ON DELETE cascade ON UPDATE no action; - EXCEPTION - WHEN duplicate_object THEN null; - END $$; - - DO $$ BEGIN - ALTER TABLE "cms-payload"."posts" ADD CONSTRAINT "posts_thumb_id_media_id_fk" FOREIGN KEY ("thumb_id") REFERENCES "cms-payload"."media"("id") ON DELETE set null ON UPDATE no action; - EXCEPTION - WHEN duplicate_object THEN null; - END $$; - - DO $$ BEGIN - ALTER TABLE "cms-payload"."posts" ADD CONSTRAINT "posts_image_id_media_id_fk" FOREIGN KEY ("image_id") REFERENCES "cms-payload"."media"("id") ON DELETE set null ON UPDATE no action; - EXCEPTION - WHEN duplicate_object THEN null; - END $$; - - DO $$ BEGIN - ALTER TABLE "cms-payload"."posts" ADD CONSTRAINT "posts_meta_image_id_media_id_fk" FOREIGN KEY ("meta_image_id") REFERENCES "cms-payload"."media"("id") ON DELETE set null ON UPDATE no action; - EXCEPTION - WHEN duplicate_object THEN null; - END $$; - - DO $$ BEGIN - ALTER TABLE "cms-payload"."posts_rels" ADD CONSTRAINT "posts_rels_parent_fk" FOREIGN KEY ("parent_id") REFERENCES "cms-payload"."posts"("id") ON DELETE cascade ON UPDATE no action; - EXCEPTION - WHEN duplicate_object THEN null; - END $$; - - DO $$ BEGIN - ALTER TABLE "cms-payload"."posts_rels" ADD CONSTRAINT "posts_rels_categories_fk" FOREIGN KEY ("categories_id") REFERENCES "cms-payload"."categories"("id") ON DELETE cascade ON UPDATE no action; - EXCEPTION - WHEN duplicate_object THEN null; - END $$; - - DO $$ BEGIN - ALTER TABLE "cms-payload"."posts_rels" ADD CONSTRAINT "posts_rels_authors_fk" FOREIGN KEY ("authors_id") REFERENCES "cms-payload"."authors"("id") ON DELETE cascade ON UPDATE no action; - EXCEPTION - WHEN duplicate_object THEN null; - END $$; - - DO $$ BEGIN - ALTER TABLE "cms-payload"."posts_rels" ADD CONSTRAINT "posts_rels_tags_fk" FOREIGN KEY ("tags_id") REFERENCES "cms-payload"."tags"("id") ON DELETE cascade ON UPDATE no action; - EXCEPTION - WHEN duplicate_object THEN null; - END $$; - - DO $$ BEGIN - ALTER TABLE "cms-payload"."_posts_v" ADD CONSTRAINT "_posts_v_parent_id_posts_id_fk" FOREIGN KEY ("parent_id") REFERENCES "cms-payload"."posts"("id") ON DELETE set null ON UPDATE no action; - EXCEPTION - WHEN duplicate_object THEN null; - END $$; - - DO $$ BEGIN - ALTER TABLE "cms-payload"."_posts_v" ADD CONSTRAINT "_posts_v_version_thumb_id_media_id_fk" FOREIGN KEY ("version_thumb_id") REFERENCES "cms-payload"."media"("id") ON DELETE set null ON UPDATE no action; - EXCEPTION - WHEN duplicate_object THEN null; - END $$; - - DO $$ BEGIN - ALTER TABLE "cms-payload"."_posts_v" ADD CONSTRAINT "_posts_v_version_image_id_media_id_fk" FOREIGN KEY ("version_image_id") REFERENCES "cms-payload"."media"("id") ON DELETE set null ON UPDATE no action; - EXCEPTION - WHEN duplicate_object THEN null; - END $$; - - DO $$ BEGIN - ALTER TABLE "cms-payload"."_posts_v" ADD CONSTRAINT "_posts_v_version_meta_image_id_media_id_fk" FOREIGN KEY ("version_meta_image_id") REFERENCES "cms-payload"."media"("id") ON DELETE set null ON UPDATE no action; - EXCEPTION - WHEN duplicate_object THEN null; - END $$; - - DO $$ BEGIN - ALTER TABLE "cms-payload"."_posts_v_rels" ADD CONSTRAINT "_posts_v_rels_parent_fk" FOREIGN KEY ("parent_id") REFERENCES "cms-payload"."_posts_v"("id") ON DELETE cascade ON UPDATE no action; - EXCEPTION - WHEN duplicate_object THEN null; - END $$; - - DO $$ BEGIN - ALTER TABLE "cms-payload"."_posts_v_rels" ADD CONSTRAINT "_posts_v_rels_categories_fk" FOREIGN KEY ("categories_id") REFERENCES "cms-payload"."categories"("id") ON DELETE cascade ON UPDATE no action; - EXCEPTION - WHEN duplicate_object THEN null; - END $$; - - DO $$ BEGIN - ALTER TABLE "cms-payload"."_posts_v_rels" ADD CONSTRAINT "_posts_v_rels_authors_fk" FOREIGN KEY ("authors_id") REFERENCES "cms-payload"."authors"("id") ON DELETE cascade ON UPDATE no action; - EXCEPTION - WHEN duplicate_object THEN null; - END $$; - - DO $$ BEGIN - ALTER TABLE "cms-payload"."_posts_v_rels" ADD CONSTRAINT "_posts_v_rels_tags_fk" FOREIGN KEY ("tags_id") REFERENCES "cms-payload"."tags"("id") ON DELETE cascade ON UPDATE no action; - EXCEPTION - WHEN duplicate_object THEN null; - END $$; - - DO $$ BEGIN - ALTER TABLE "cms-payload"."users_roles" ADD CONSTRAINT "users_roles_parent_fk" FOREIGN KEY ("parent_id") REFERENCES "cms-payload"."users"("id") ON DELETE cascade ON UPDATE no action; - EXCEPTION - WHEN duplicate_object THEN null; - END $$; - - DO $$ BEGIN - ALTER TABLE "cms-payload"."payload_jobs_log" ADD CONSTRAINT "payload_jobs_log_parent_id_fk" FOREIGN KEY ("_parent_id") REFERENCES "cms-payload"."payload_jobs"("id") ON DELETE cascade ON UPDATE no action; - EXCEPTION - WHEN duplicate_object THEN null; - END $$; - - DO $$ BEGIN - ALTER TABLE "cms-payload"."payload_locked_documents_rels" ADD CONSTRAINT "payload_locked_documents_rels_parent_fk" FOREIGN KEY ("parent_id") REFERENCES "cms-payload"."payload_locked_documents"("id") ON DELETE cascade ON UPDATE no action; - EXCEPTION - WHEN duplicate_object THEN null; - END $$; - - DO $$ BEGIN - ALTER TABLE "cms-payload"."payload_locked_documents_rels" ADD CONSTRAINT "payload_locked_documents_rels_authors_fk" FOREIGN KEY ("authors_id") REFERENCES "cms-payload"."authors"("id") ON DELETE cascade ON UPDATE no action; - EXCEPTION - WHEN duplicate_object THEN null; - END $$; - - DO $$ BEGIN - ALTER TABLE "cms-payload"."payload_locked_documents_rels" ADD CONSTRAINT "payload_locked_documents_rels_categories_fk" FOREIGN KEY ("categories_id") REFERENCES "cms-payload"."categories"("id") ON DELETE cascade ON UPDATE no action; - EXCEPTION - WHEN duplicate_object THEN null; - END $$; - - DO $$ BEGIN - ALTER TABLE "cms-payload"."payload_locked_documents_rels" ADD CONSTRAINT "payload_locked_documents_rels_customers_fk" FOREIGN KEY ("customers_id") REFERENCES "cms-payload"."customers"("id") ON DELETE cascade ON UPDATE no action; - EXCEPTION - WHEN duplicate_object THEN null; - END $$; - - DO $$ BEGIN - ALTER TABLE "cms-payload"."payload_locked_documents_rels" ADD CONSTRAINT "payload_locked_documents_rels_events_fk" FOREIGN KEY ("events_id") REFERENCES "cms-payload"."events"("id") ON DELETE cascade ON UPDATE no action; - EXCEPTION - WHEN duplicate_object THEN null; - END $$; - - DO $$ BEGIN - ALTER TABLE "cms-payload"."payload_locked_documents_rels" ADD CONSTRAINT "payload_locked_documents_rels_media_fk" FOREIGN KEY ("media_id") REFERENCES "cms-payload"."media"("id") ON DELETE cascade ON UPDATE no action; - EXCEPTION - WHEN duplicate_object THEN null; - END $$; - - DO $$ BEGIN - ALTER TABLE "cms-payload"."payload_locked_documents_rels" ADD CONSTRAINT "payload_locked_documents_rels_posts_fk" FOREIGN KEY ("posts_id") REFERENCES "cms-payload"."posts"("id") ON DELETE cascade ON UPDATE no action; - EXCEPTION - WHEN duplicate_object THEN null; - END $$; - - DO $$ BEGIN - ALTER TABLE "cms-payload"."payload_locked_documents_rels" ADD CONSTRAINT "payload_locked_documents_rels_tags_fk" FOREIGN KEY ("tags_id") REFERENCES "cms-payload"."tags"("id") ON DELETE cascade ON UPDATE no action; - EXCEPTION - WHEN duplicate_object THEN null; - END $$; - - DO $$ BEGIN - ALTER TABLE "cms-payload"."payload_locked_documents_rels" ADD CONSTRAINT "payload_locked_documents_rels_users_fk" FOREIGN KEY ("users_id") REFERENCES "cms-payload"."users"("id") ON DELETE cascade ON UPDATE no action; - EXCEPTION - WHEN duplicate_object THEN null; - END $$; - - DO $$ BEGIN - ALTER TABLE "cms-payload"."payload_locked_documents_rels" ADD CONSTRAINT "payload_locked_documents_rels_payload_jobs_fk" FOREIGN KEY ("payload_jobs_id") REFERENCES "cms-payload"."payload_jobs"("id") ON DELETE cascade ON UPDATE no action; - EXCEPTION - WHEN duplicate_object THEN null; - END $$; - - DO $$ BEGIN - ALTER TABLE "cms-payload"."payload_preferences_rels" ADD CONSTRAINT "payload_preferences_rels_parent_fk" FOREIGN KEY ("parent_id") REFERENCES "cms-payload"."payload_preferences"("id") ON DELETE cascade ON UPDATE no action; - EXCEPTION - WHEN duplicate_object THEN null; - END $$; - - DO $$ BEGIN - ALTER TABLE "cms-payload"."payload_preferences_rels" ADD CONSTRAINT "payload_preferences_rels_users_fk" FOREIGN KEY ("users_id") REFERENCES "cms-payload"."users"("id") ON DELETE cascade ON UPDATE no action; - EXCEPTION - WHEN duplicate_object THEN null; - END $$; - - CREATE INDEX IF NOT EXISTS "authors_author_image_url_idx" ON "cms-payload"."authors" USING btree ("author_image_url_id"); - CREATE INDEX IF NOT EXISTS "authors_updated_at_idx" ON "cms-payload"."authors" USING btree ("updated_at"); - CREATE INDEX IF NOT EXISTS "authors_created_at_idx" ON "cms-payload"."authors" USING btree ("created_at"); - CREATE INDEX IF NOT EXISTS "categories_breadcrumbs_order_idx" ON "cms-payload"."categories_breadcrumbs" USING btree ("_order"); - CREATE INDEX IF NOT EXISTS "categories_breadcrumbs_parent_id_idx" ON "cms-payload"."categories_breadcrumbs" USING btree ("_parent_id"); - CREATE INDEX IF NOT EXISTS "categories_breadcrumbs_doc_idx" ON "cms-payload"."categories_breadcrumbs" USING btree ("doc_id"); - CREATE INDEX IF NOT EXISTS "categories_parent_idx" ON "cms-payload"."categories" USING btree ("parent_id"); - CREATE INDEX IF NOT EXISTS "categories_updated_at_idx" ON "cms-payload"."categories" USING btree ("updated_at"); - CREATE INDEX IF NOT EXISTS "categories_created_at_idx" ON "cms-payload"."categories" USING btree ("created_at"); - CREATE INDEX IF NOT EXISTS "customers_stats_order_idx" ON "cms-payload"."customers_stats" USING btree ("_order"); - CREATE INDEX IF NOT EXISTS "customers_stats_parent_id_idx" ON "cms-payload"."customers_stats" USING btree ("_parent_id"); - CREATE INDEX IF NOT EXISTS "customers_misc_order_idx" ON "cms-payload"."customers_misc" USING btree ("_order"); - CREATE INDEX IF NOT EXISTS "customers_misc_parent_id_idx" ON "cms-payload"."customers_misc" USING btree ("_parent_id"); - CREATE INDEX IF NOT EXISTS "customers_industry_order_idx" ON "cms-payload"."customers_industry" USING btree ("order"); - CREATE INDEX IF NOT EXISTS "customers_industry_parent_idx" ON "cms-payload"."customers_industry" USING btree ("parent_id"); - CREATE INDEX IF NOT EXISTS "customers_supabase_products_order_idx" ON "cms-payload"."customers_supabase_products" USING btree ("order"); - CREATE INDEX IF NOT EXISTS "customers_supabase_products_parent_idx" ON "cms-payload"."customers_supabase_products" USING btree ("parent_id"); - CREATE INDEX IF NOT EXISTS "customers_slug_idx" ON "cms-payload"."customers" USING btree ("slug"); - CREATE INDEX IF NOT EXISTS "customers_meta_meta_image_idx" ON "cms-payload"."customers" USING btree ("meta_image_id"); - CREATE INDEX IF NOT EXISTS "customers_logo_idx" ON "cms-payload"."customers" USING btree ("logo_id"); - CREATE INDEX IF NOT EXISTS "customers_logo_inverse_idx" ON "cms-payload"."customers" USING btree ("logo_inverse_id"); - CREATE INDEX IF NOT EXISTS "customers_updated_at_idx" ON "cms-payload"."customers" USING btree ("updated_at"); - CREATE INDEX IF NOT EXISTS "customers_created_at_idx" ON "cms-payload"."customers" USING btree ("created_at"); - CREATE INDEX IF NOT EXISTS "customers__status_idx" ON "cms-payload"."customers" USING btree ("_status"); - CREATE INDEX IF NOT EXISTS "_customers_v_version_stats_order_idx" ON "cms-payload"."_customers_v_version_stats" USING btree ("_order"); - CREATE INDEX IF NOT EXISTS "_customers_v_version_stats_parent_id_idx" ON "cms-payload"."_customers_v_version_stats" USING btree ("_parent_id"); - CREATE INDEX IF NOT EXISTS "_customers_v_version_misc_order_idx" ON "cms-payload"."_customers_v_version_misc" USING btree ("_order"); - CREATE INDEX IF NOT EXISTS "_customers_v_version_misc_parent_id_idx" ON "cms-payload"."_customers_v_version_misc" USING btree ("_parent_id"); - CREATE INDEX IF NOT EXISTS "_customers_v_version_industry_order_idx" ON "cms-payload"."_customers_v_version_industry" USING btree ("order"); - CREATE INDEX IF NOT EXISTS "_customers_v_version_industry_parent_idx" ON "cms-payload"."_customers_v_version_industry" USING btree ("parent_id"); - CREATE INDEX IF NOT EXISTS "_customers_v_version_supabase_products_order_idx" ON "cms-payload"."_customers_v_version_supabase_products" USING btree ("order"); - CREATE INDEX IF NOT EXISTS "_customers_v_version_supabase_products_parent_idx" ON "cms-payload"."_customers_v_version_supabase_products" USING btree ("parent_id"); - CREATE INDEX IF NOT EXISTS "_customers_v_parent_idx" ON "cms-payload"."_customers_v" USING btree ("parent_id"); - CREATE INDEX IF NOT EXISTS "_customers_v_version_version_slug_idx" ON "cms-payload"."_customers_v" USING btree ("version_slug"); - CREATE INDEX IF NOT EXISTS "_customers_v_version_meta_version_meta_image_idx" ON "cms-payload"."_customers_v" USING btree ("version_meta_image_id"); - CREATE INDEX IF NOT EXISTS "_customers_v_version_version_logo_idx" ON "cms-payload"."_customers_v" USING btree ("version_logo_id"); - CREATE INDEX IF NOT EXISTS "_customers_v_version_version_logo_inverse_idx" ON "cms-payload"."_customers_v" USING btree ("version_logo_inverse_id"); - CREATE INDEX IF NOT EXISTS "_customers_v_version_version_updated_at_idx" ON "cms-payload"."_customers_v" USING btree ("version_updated_at"); - CREATE INDEX IF NOT EXISTS "_customers_v_version_version_created_at_idx" ON "cms-payload"."_customers_v" USING btree ("version_created_at"); - CREATE INDEX IF NOT EXISTS "_customers_v_version_version__status_idx" ON "cms-payload"."_customers_v" USING btree ("version__status"); - CREATE INDEX IF NOT EXISTS "_customers_v_created_at_idx" ON "cms-payload"."_customers_v" USING btree ("created_at"); - CREATE INDEX IF NOT EXISTS "_customers_v_updated_at_idx" ON "cms-payload"."_customers_v" USING btree ("updated_at"); - CREATE INDEX IF NOT EXISTS "_customers_v_latest_idx" ON "cms-payload"."_customers_v" USING btree ("latest"); - CREATE INDEX IF NOT EXISTS "_customers_v_autosave_idx" ON "cms-payload"."_customers_v" USING btree ("autosave"); - CREATE INDEX IF NOT EXISTS "events_type_order_idx" ON "cms-payload"."events_type" USING btree ("order"); - CREATE INDEX IF NOT EXISTS "events_type_parent_idx" ON "cms-payload"."events_type" USING btree ("parent_id"); - CREATE INDEX IF NOT EXISTS "events_slug_idx" ON "cms-payload"."events" USING btree ("slug"); - CREATE INDEX IF NOT EXISTS "events_thumb_idx" ON "cms-payload"."events" USING btree ("thumb_id"); - CREATE INDEX IF NOT EXISTS "events_image_idx" ON "cms-payload"."events" USING btree ("image_id"); - CREATE INDEX IF NOT EXISTS "events_company_company_logo_idx" ON "cms-payload"."events" USING btree ("company_logo_id"); - CREATE INDEX IF NOT EXISTS "events_company_company_logo_light_idx" ON "cms-payload"."events" USING btree ("company_logo_light_id"); - CREATE INDEX IF NOT EXISTS "events_meta_meta_image_idx" ON "cms-payload"."events" USING btree ("meta_image_id"); - CREATE INDEX IF NOT EXISTS "events_updated_at_idx" ON "cms-payload"."events" USING btree ("updated_at"); - CREATE INDEX IF NOT EXISTS "events_created_at_idx" ON "cms-payload"."events" USING btree ("created_at"); - CREATE INDEX IF NOT EXISTS "events__status_idx" ON "cms-payload"."events" USING btree ("_status"); - CREATE INDEX IF NOT EXISTS "events_rels_order_idx" ON "cms-payload"."events_rels" USING btree ("order"); - CREATE INDEX IF NOT EXISTS "events_rels_parent_idx" ON "cms-payload"."events_rels" USING btree ("parent_id"); - CREATE INDEX IF NOT EXISTS "events_rels_path_idx" ON "cms-payload"."events_rels" USING btree ("path"); - CREATE INDEX IF NOT EXISTS "events_rels_authors_id_idx" ON "cms-payload"."events_rels" USING btree ("authors_id"); - CREATE INDEX IF NOT EXISTS "_events_v_version_type_order_idx" ON "cms-payload"."_events_v_version_type" USING btree ("order"); - CREATE INDEX IF NOT EXISTS "_events_v_version_type_parent_idx" ON "cms-payload"."_events_v_version_type" USING btree ("parent_id"); - CREATE INDEX IF NOT EXISTS "_events_v_parent_idx" ON "cms-payload"."_events_v" USING btree ("parent_id"); - CREATE INDEX IF NOT EXISTS "_events_v_version_version_slug_idx" ON "cms-payload"."_events_v" USING btree ("version_slug"); - CREATE INDEX IF NOT EXISTS "_events_v_version_version_thumb_idx" ON "cms-payload"."_events_v" USING btree ("version_thumb_id"); - CREATE INDEX IF NOT EXISTS "_events_v_version_version_image_idx" ON "cms-payload"."_events_v" USING btree ("version_image_id"); - CREATE INDEX IF NOT EXISTS "_events_v_version_company_version_company_logo_idx" ON "cms-payload"."_events_v" USING btree ("version_company_logo_id"); - CREATE INDEX IF NOT EXISTS "_events_v_version_company_version_company_logo_light_idx" ON "cms-payload"."_events_v" USING btree ("version_company_logo_light_id"); - CREATE INDEX IF NOT EXISTS "_events_v_version_meta_version_meta_image_idx" ON "cms-payload"."_events_v" USING btree ("version_meta_image_id"); - CREATE INDEX IF NOT EXISTS "_events_v_version_version_updated_at_idx" ON "cms-payload"."_events_v" USING btree ("version_updated_at"); - CREATE INDEX IF NOT EXISTS "_events_v_version_version_created_at_idx" ON "cms-payload"."_events_v" USING btree ("version_created_at"); - CREATE INDEX IF NOT EXISTS "_events_v_version_version__status_idx" ON "cms-payload"."_events_v" USING btree ("version__status"); - CREATE INDEX IF NOT EXISTS "_events_v_created_at_idx" ON "cms-payload"."_events_v" USING btree ("created_at"); - CREATE INDEX IF NOT EXISTS "_events_v_updated_at_idx" ON "cms-payload"."_events_v" USING btree ("updated_at"); - CREATE INDEX IF NOT EXISTS "_events_v_latest_idx" ON "cms-payload"."_events_v" USING btree ("latest"); - CREATE INDEX IF NOT EXISTS "_events_v_autosave_idx" ON "cms-payload"."_events_v" USING btree ("autosave"); - CREATE INDEX IF NOT EXISTS "_events_v_rels_order_idx" ON "cms-payload"."_events_v_rels" USING btree ("order"); - CREATE INDEX IF NOT EXISTS "_events_v_rels_parent_idx" ON "cms-payload"."_events_v_rels" USING btree ("parent_id"); - CREATE INDEX IF NOT EXISTS "_events_v_rels_path_idx" ON "cms-payload"."_events_v_rels" USING btree ("path"); - CREATE INDEX IF NOT EXISTS "_events_v_rels_authors_id_idx" ON "cms-payload"."_events_v_rels" USING btree ("authors_id"); - CREATE INDEX IF NOT EXISTS "media_updated_at_idx" ON "cms-payload"."media" USING btree ("updated_at"); - CREATE INDEX IF NOT EXISTS "media_created_at_idx" ON "cms-payload"."media" USING btree ("created_at"); - CREATE UNIQUE INDEX IF NOT EXISTS "media_filename_idx" ON "cms-payload"."media" USING btree ("filename"); - CREATE INDEX IF NOT EXISTS "media_sizes_thumbnail_sizes_thumbnail_filename_idx" ON "cms-payload"."media" USING btree ("sizes_thumbnail_filename"); - CREATE INDEX IF NOT EXISTS "media_sizes_square_sizes_square_filename_idx" ON "cms-payload"."media" USING btree ("sizes_square_filename"); - CREATE INDEX IF NOT EXISTS "media_sizes_small_sizes_small_filename_idx" ON "cms-payload"."media" USING btree ("sizes_small_filename"); - CREATE INDEX IF NOT EXISTS "media_sizes_medium_sizes_medium_filename_idx" ON "cms-payload"."media" USING btree ("sizes_medium_filename"); - CREATE INDEX IF NOT EXISTS "media_sizes_large_sizes_large_filename_idx" ON "cms-payload"."media" USING btree ("sizes_large_filename"); - CREATE INDEX IF NOT EXISTS "media_sizes_xlarge_sizes_xlarge_filename_idx" ON "cms-payload"."media" USING btree ("sizes_xlarge_filename"); - CREATE INDEX IF NOT EXISTS "media_sizes_og_sizes_og_filename_idx" ON "cms-payload"."media" USING btree ("sizes_og_filename"); - CREATE INDEX IF NOT EXISTS "posts_slug_idx" ON "cms-payload"."posts" USING btree ("slug"); - CREATE INDEX IF NOT EXISTS "posts_thumb_idx" ON "cms-payload"."posts" USING btree ("thumb_id"); - CREATE INDEX IF NOT EXISTS "posts_image_idx" ON "cms-payload"."posts" USING btree ("image_id"); - CREATE INDEX IF NOT EXISTS "posts_meta_meta_image_idx" ON "cms-payload"."posts" USING btree ("meta_image_id"); - CREATE INDEX IF NOT EXISTS "posts_updated_at_idx" ON "cms-payload"."posts" USING btree ("updated_at"); - CREATE INDEX IF NOT EXISTS "posts_created_at_idx" ON "cms-payload"."posts" USING btree ("created_at"); - CREATE INDEX IF NOT EXISTS "posts__status_idx" ON "cms-payload"."posts" USING btree ("_status"); - CREATE INDEX IF NOT EXISTS "posts_rels_order_idx" ON "cms-payload"."posts_rels" USING btree ("order"); - CREATE INDEX IF NOT EXISTS "posts_rels_parent_idx" ON "cms-payload"."posts_rels" USING btree ("parent_id"); - CREATE INDEX IF NOT EXISTS "posts_rels_path_idx" ON "cms-payload"."posts_rels" USING btree ("path"); - CREATE INDEX IF NOT EXISTS "posts_rels_categories_id_idx" ON "cms-payload"."posts_rels" USING btree ("categories_id"); - CREATE INDEX IF NOT EXISTS "posts_rels_authors_id_idx" ON "cms-payload"."posts_rels" USING btree ("authors_id"); - CREATE INDEX IF NOT EXISTS "posts_rels_tags_id_idx" ON "cms-payload"."posts_rels" USING btree ("tags_id"); - CREATE INDEX IF NOT EXISTS "_posts_v_parent_idx" ON "cms-payload"."_posts_v" USING btree ("parent_id"); - CREATE INDEX IF NOT EXISTS "_posts_v_version_version_slug_idx" ON "cms-payload"."_posts_v" USING btree ("version_slug"); - CREATE INDEX IF NOT EXISTS "_posts_v_version_version_thumb_idx" ON "cms-payload"."_posts_v" USING btree ("version_thumb_id"); - CREATE INDEX IF NOT EXISTS "_posts_v_version_version_image_idx" ON "cms-payload"."_posts_v" USING btree ("version_image_id"); - CREATE INDEX IF NOT EXISTS "_posts_v_version_meta_version_meta_image_idx" ON "cms-payload"."_posts_v" USING btree ("version_meta_image_id"); - CREATE INDEX IF NOT EXISTS "_posts_v_version_version_updated_at_idx" ON "cms-payload"."_posts_v" USING btree ("version_updated_at"); - CREATE INDEX IF NOT EXISTS "_posts_v_version_version_created_at_idx" ON "cms-payload"."_posts_v" USING btree ("version_created_at"); - CREATE INDEX IF NOT EXISTS "_posts_v_version_version__status_idx" ON "cms-payload"."_posts_v" USING btree ("version__status"); - CREATE INDEX IF NOT EXISTS "_posts_v_created_at_idx" ON "cms-payload"."_posts_v" USING btree ("created_at"); - CREATE INDEX IF NOT EXISTS "_posts_v_updated_at_idx" ON "cms-payload"."_posts_v" USING btree ("updated_at"); - CREATE INDEX IF NOT EXISTS "_posts_v_latest_idx" ON "cms-payload"."_posts_v" USING btree ("latest"); - CREATE INDEX IF NOT EXISTS "_posts_v_autosave_idx" ON "cms-payload"."_posts_v" USING btree ("autosave"); - CREATE INDEX IF NOT EXISTS "_posts_v_rels_order_idx" ON "cms-payload"."_posts_v_rels" USING btree ("order"); - CREATE INDEX IF NOT EXISTS "_posts_v_rels_parent_idx" ON "cms-payload"."_posts_v_rels" USING btree ("parent_id"); - CREATE INDEX IF NOT EXISTS "_posts_v_rels_path_idx" ON "cms-payload"."_posts_v_rels" USING btree ("path"); - CREATE INDEX IF NOT EXISTS "_posts_v_rels_categories_id_idx" ON "cms-payload"."_posts_v_rels" USING btree ("categories_id"); - CREATE INDEX IF NOT EXISTS "_posts_v_rels_authors_id_idx" ON "cms-payload"."_posts_v_rels" USING btree ("authors_id"); - CREATE INDEX IF NOT EXISTS "_posts_v_rels_tags_id_idx" ON "cms-payload"."_posts_v_rels" USING btree ("tags_id"); - CREATE INDEX IF NOT EXISTS "tags_updated_at_idx" ON "cms-payload"."tags" USING btree ("updated_at"); - CREATE INDEX IF NOT EXISTS "tags_created_at_idx" ON "cms-payload"."tags" USING btree ("created_at"); - CREATE INDEX IF NOT EXISTS "users_roles_order_idx" ON "cms-payload"."users_roles" USING btree ("order"); - CREATE INDEX IF NOT EXISTS "users_roles_parent_idx" ON "cms-payload"."users_roles" USING btree ("parent_id"); - CREATE INDEX IF NOT EXISTS "users_updated_at_idx" ON "cms-payload"."users" USING btree ("updated_at"); - CREATE INDEX IF NOT EXISTS "users_created_at_idx" ON "cms-payload"."users" USING btree ("created_at"); - CREATE UNIQUE INDEX IF NOT EXISTS "users_email_idx" ON "cms-payload"."users" USING btree ("email"); - CREATE INDEX IF NOT EXISTS "payload_jobs_log_order_idx" ON "cms-payload"."payload_jobs_log" USING btree ("_order"); - CREATE INDEX IF NOT EXISTS "payload_jobs_log_parent_id_idx" ON "cms-payload"."payload_jobs_log" USING btree ("_parent_id"); - CREATE INDEX IF NOT EXISTS "payload_jobs_completed_at_idx" ON "cms-payload"."payload_jobs" USING btree ("completed_at"); - CREATE INDEX IF NOT EXISTS "payload_jobs_total_tried_idx" ON "cms-payload"."payload_jobs" USING btree ("total_tried"); - CREATE INDEX IF NOT EXISTS "payload_jobs_has_error_idx" ON "cms-payload"."payload_jobs" USING btree ("has_error"); - CREATE INDEX IF NOT EXISTS "payload_jobs_task_slug_idx" ON "cms-payload"."payload_jobs" USING btree ("task_slug"); - CREATE INDEX IF NOT EXISTS "payload_jobs_queue_idx" ON "cms-payload"."payload_jobs" USING btree ("queue"); - CREATE INDEX IF NOT EXISTS "payload_jobs_wait_until_idx" ON "cms-payload"."payload_jobs" USING btree ("wait_until"); - CREATE INDEX IF NOT EXISTS "payload_jobs_processing_idx" ON "cms-payload"."payload_jobs" USING btree ("processing"); - CREATE INDEX IF NOT EXISTS "payload_jobs_updated_at_idx" ON "cms-payload"."payload_jobs" USING btree ("updated_at"); - CREATE INDEX IF NOT EXISTS "payload_jobs_created_at_idx" ON "cms-payload"."payload_jobs" USING btree ("created_at"); - CREATE INDEX IF NOT EXISTS "payload_locked_documents_global_slug_idx" ON "cms-payload"."payload_locked_documents" USING btree ("global_slug"); - CREATE INDEX IF NOT EXISTS "payload_locked_documents_updated_at_idx" ON "cms-payload"."payload_locked_documents" USING btree ("updated_at"); - CREATE INDEX IF NOT EXISTS "payload_locked_documents_created_at_idx" ON "cms-payload"."payload_locked_documents" USING btree ("created_at"); - CREATE INDEX IF NOT EXISTS "payload_locked_documents_rels_order_idx" ON "cms-payload"."payload_locked_documents_rels" USING btree ("order"); - CREATE INDEX IF NOT EXISTS "payload_locked_documents_rels_parent_idx" ON "cms-payload"."payload_locked_documents_rels" USING btree ("parent_id"); - CREATE INDEX IF NOT EXISTS "payload_locked_documents_rels_path_idx" ON "cms-payload"."payload_locked_documents_rels" USING btree ("path"); - CREATE INDEX IF NOT EXISTS "payload_locked_documents_rels_authors_id_idx" ON "cms-payload"."payload_locked_documents_rels" USING btree ("authors_id"); - CREATE INDEX IF NOT EXISTS "payload_locked_documents_rels_categories_id_idx" ON "cms-payload"."payload_locked_documents_rels" USING btree ("categories_id"); - CREATE INDEX IF NOT EXISTS "payload_locked_documents_rels_customers_id_idx" ON "cms-payload"."payload_locked_documents_rels" USING btree ("customers_id"); - CREATE INDEX IF NOT EXISTS "payload_locked_documents_rels_events_id_idx" ON "cms-payload"."payload_locked_documents_rels" USING btree ("events_id"); - CREATE INDEX IF NOT EXISTS "payload_locked_documents_rels_media_id_idx" ON "cms-payload"."payload_locked_documents_rels" USING btree ("media_id"); - CREATE INDEX IF NOT EXISTS "payload_locked_documents_rels_posts_id_idx" ON "cms-payload"."payload_locked_documents_rels" USING btree ("posts_id"); - CREATE INDEX IF NOT EXISTS "payload_locked_documents_rels_tags_id_idx" ON "cms-payload"."payload_locked_documents_rels" USING btree ("tags_id"); - CREATE INDEX IF NOT EXISTS "payload_locked_documents_rels_users_id_idx" ON "cms-payload"."payload_locked_documents_rels" USING btree ("users_id"); - CREATE INDEX IF NOT EXISTS "payload_locked_documents_rels_payload_jobs_id_idx" ON "cms-payload"."payload_locked_documents_rels" USING btree ("payload_jobs_id"); - CREATE INDEX IF NOT EXISTS "payload_preferences_key_idx" ON "cms-payload"."payload_preferences" USING btree ("key"); - CREATE INDEX IF NOT EXISTS "payload_preferences_updated_at_idx" ON "cms-payload"."payload_preferences" USING btree ("updated_at"); - CREATE INDEX IF NOT EXISTS "payload_preferences_created_at_idx" ON "cms-payload"."payload_preferences" USING btree ("created_at"); - CREATE INDEX IF NOT EXISTS "payload_preferences_rels_order_idx" ON "cms-payload"."payload_preferences_rels" USING btree ("order"); - CREATE INDEX IF NOT EXISTS "payload_preferences_rels_parent_idx" ON "cms-payload"."payload_preferences_rels" USING btree ("parent_id"); - CREATE INDEX IF NOT EXISTS "payload_preferences_rels_path_idx" ON "cms-payload"."payload_preferences_rels" USING btree ("path"); - CREATE INDEX IF NOT EXISTS "payload_preferences_rels_users_id_idx" ON "cms-payload"."payload_preferences_rels" USING btree ("users_id"); - CREATE INDEX IF NOT EXISTS "payload_migrations_updated_at_idx" ON "cms-payload"."payload_migrations" USING btree ("updated_at"); - CREATE INDEX IF NOT EXISTS "payload_migrations_created_at_idx" ON "cms-payload"."payload_migrations" USING btree ("created_at");`) -} - -export async function down({ db }: MigrateDownArgs): Promise { - await db.execute(sql` - DROP TABLE "cms-payload"."authors" CASCADE; - DROP TABLE "cms-payload"."categories_breadcrumbs" CASCADE; - DROP TABLE "cms-payload"."categories" CASCADE; - DROP TABLE "cms-payload"."customers_stats" CASCADE; - DROP TABLE "cms-payload"."customers_misc" CASCADE; - DROP TABLE "cms-payload"."customers_industry" CASCADE; - DROP TABLE "cms-payload"."customers_supabase_products" CASCADE; - DROP TABLE "cms-payload"."customers" CASCADE; - DROP TABLE "cms-payload"."_customers_v_version_stats" CASCADE; - DROP TABLE "cms-payload"."_customers_v_version_misc" CASCADE; - DROP TABLE "cms-payload"."_customers_v_version_industry" CASCADE; - DROP TABLE "cms-payload"."_customers_v_version_supabase_products" CASCADE; - DROP TABLE "cms-payload"."_customers_v" CASCADE; - DROP TABLE "cms-payload"."events_type" CASCADE; - DROP TABLE "cms-payload"."events" CASCADE; - DROP TABLE "cms-payload"."events_rels" CASCADE; - DROP TABLE "cms-payload"."_events_v_version_type" CASCADE; - DROP TABLE "cms-payload"."_events_v" CASCADE; - DROP TABLE "cms-payload"."_events_v_rels" CASCADE; - DROP TABLE "cms-payload"."media" CASCADE; - DROP TABLE "cms-payload"."posts" CASCADE; - DROP TABLE "cms-payload"."posts_rels" CASCADE; - DROP TABLE "cms-payload"."_posts_v" CASCADE; - DROP TABLE "cms-payload"."_posts_v_rels" CASCADE; - DROP TABLE "cms-payload"."tags" CASCADE; - DROP TABLE "cms-payload"."users_roles" CASCADE; - DROP TABLE "cms-payload"."users" CASCADE; - DROP TABLE "cms-payload"."payload_jobs_log" CASCADE; - DROP TABLE "cms-payload"."payload_jobs" CASCADE; - DROP TABLE "cms-payload"."payload_locked_documents" CASCADE; - DROP TABLE "cms-payload"."payload_locked_documents_rels" CASCADE; - DROP TABLE "cms-payload"."payload_preferences" CASCADE; - DROP TABLE "cms-payload"."payload_preferences_rels" CASCADE; - DROP TABLE "cms-payload"."payload_migrations" CASCADE; - DROP TYPE "cms-payload"."enum_customers_industry"; - DROP TYPE "cms-payload"."enum_customers_supabase_products"; - DROP TYPE "cms-payload"."enum_customers_company_size"; - DROP TYPE "cms-payload"."enum_customers_region"; - DROP TYPE "cms-payload"."enum_customers_status"; - DROP TYPE "cms-payload"."enum__customers_v_version_industry"; - DROP TYPE "cms-payload"."enum__customers_v_version_supabase_products"; - DROP TYPE "cms-payload"."enum__customers_v_version_company_size"; - DROP TYPE "cms-payload"."enum__customers_v_version_region"; - DROP TYPE "cms-payload"."enum__customers_v_version_status"; - DROP TYPE "cms-payload"."enum_events_type"; - DROP TYPE "cms-payload"."enum_events_timezone"; - DROP TYPE "cms-payload"."enum_events_link_target"; - DROP TYPE "cms-payload"."enum_events_main_cta_target"; - DROP TYPE "cms-payload"."enum_events_status"; - DROP TYPE "cms-payload"."enum__events_v_version_type"; - DROP TYPE "cms-payload"."enum__events_v_version_timezone"; - DROP TYPE "cms-payload"."enum__events_v_version_link_target"; - DROP TYPE "cms-payload"."enum__events_v_version_main_cta_target"; - DROP TYPE "cms-payload"."enum__events_v_version_status"; - DROP TYPE "cms-payload"."enum_posts_launchweek"; - DROP TYPE "cms-payload"."enum_posts_status"; - DROP TYPE "cms-payload"."enum__posts_v_version_launchweek"; - DROP TYPE "cms-payload"."enum__posts_v_version_status"; - DROP TYPE "cms-payload"."enum_users_roles"; - DROP TYPE "cms-payload"."enum_payload_jobs_log_task_slug"; - DROP TYPE "cms-payload"."enum_payload_jobs_log_state"; - DROP TYPE "cms-payload"."enum_payload_jobs_task_slug";`) -} diff --git a/apps/cms/src/migrations/20250529_103319.json b/apps/cms/src/migrations/20250619_155345.json similarity index 95% rename from apps/cms/src/migrations/20250529_103319.json rename to apps/cms/src/migrations/20250619_155345.json index 04fb0783181..7fec1a1285b 100644 --- a/apps/cms/src/migrations/20250529_103319.json +++ b/apps/cms/src/migrations/20250619_155345.json @@ -1,12 +1,12 @@ { - "id": "b20fdabe-061b-43a4-8abf-b401ef5818d1", + "id": "46141a51-8524-466d-9e94-70fe5c73221c", "prevId": "00000000-0000-0000-0000-000000000000", "version": "7", "dialect": "postgresql", "tables": { - "cms-payload.authors": { + "public.authors": { "name": "authors", - "schema": "cms-payload", + "schema": "", "columns": { "id": { "name": "id", @@ -117,7 +117,6 @@ "name": "authors_author_image_url_id_media_id_fk", "tableFrom": "authors", "tableTo": "media", - "schemaTo": "cms-payload", "columnsFrom": [ "author_image_url_id" ], @@ -134,9 +133,9 @@ "checkConstraints": {}, "isRLSEnabled": false }, - "cms-payload.categories_breadcrumbs": { + "public.categories_breadcrumbs": { "name": "categories_breadcrumbs", - "schema": "cms-payload", + "schema": "", "columns": { "_order": { "name": "_order", @@ -227,7 +226,6 @@ "name": "categories_breadcrumbs_doc_id_categories_id_fk", "tableFrom": "categories_breadcrumbs", "tableTo": "categories", - "schemaTo": "cms-payload", "columnsFrom": [ "doc_id" ], @@ -241,7 +239,6 @@ "name": "categories_breadcrumbs_parent_id_fk", "tableFrom": "categories_breadcrumbs", "tableTo": "categories", - "schemaTo": "cms-payload", "columnsFrom": [ "_parent_id" ], @@ -258,9 +255,9 @@ "checkConstraints": {}, "isRLSEnabled": false }, - "cms-payload.categories": { + "public.categories": { "name": "categories", - "schema": "cms-payload", + "schema": "", "columns": { "id": { "name": "id", @@ -347,7 +344,6 @@ "name": "categories_parent_id_categories_id_fk", "tableFrom": "categories", "tableTo": "categories", - "schemaTo": "cms-payload", "columnsFrom": [ "parent_id" ], @@ -364,9 +360,9 @@ "checkConstraints": {}, "isRLSEnabled": false }, - "cms-payload.customers_stats": { + "public.customers_stats": { "name": "customers_stats", - "schema": "cms-payload", + "schema": "", "columns": { "_order": { "name": "_order", @@ -436,7 +432,6 @@ "name": "customers_stats_parent_id_fk", "tableFrom": "customers_stats", "tableTo": "customers", - "schemaTo": "cms-payload", "columnsFrom": [ "_parent_id" ], @@ -453,9 +448,9 @@ "checkConstraints": {}, "isRLSEnabled": false }, - "cms-payload.customers_misc": { + "public.customers_misc": { "name": "customers_misc", - "schema": "cms-payload", + "schema": "", "columns": { "_order": { "name": "_order", @@ -525,7 +520,6 @@ "name": "customers_misc_parent_id_fk", "tableFrom": "customers_misc", "tableTo": "customers", - "schemaTo": "cms-payload", "columnsFrom": [ "_parent_id" ], @@ -542,9 +536,9 @@ "checkConstraints": {}, "isRLSEnabled": false }, - "cms-payload.customers_industry": { + "public.customers_industry": { "name": "customers_industry", - "schema": "cms-payload", + "schema": "", "columns": { "order": { "name": "order", @@ -561,7 +555,7 @@ "value": { "name": "value", "type": "enum_customers_industry", - "typeSchema": "cms-payload", + "typeSchema": "public", "primaryKey": false, "notNull": false }, @@ -609,7 +603,6 @@ "name": "customers_industry_parent_fk", "tableFrom": "customers_industry", "tableTo": "customers", - "schemaTo": "cms-payload", "columnsFrom": [ "parent_id" ], @@ -626,9 +619,9 @@ "checkConstraints": {}, "isRLSEnabled": false }, - "cms-payload.customers_supabase_products": { + "public.customers_supabase_products": { "name": "customers_supabase_products", - "schema": "cms-payload", + "schema": "", "columns": { "order": { "name": "order", @@ -645,7 +638,7 @@ "value": { "name": "value", "type": "enum_customers_supabase_products", - "typeSchema": "cms-payload", + "typeSchema": "public", "primaryKey": false, "notNull": false }, @@ -693,7 +686,6 @@ "name": "customers_supabase_products_parent_fk", "tableFrom": "customers_supabase_products", "tableTo": "customers", - "schemaTo": "cms-payload", "columnsFrom": [ "parent_id" ], @@ -710,9 +702,9 @@ "checkConstraints": {}, "isRLSEnabled": false }, - "cms-payload.customers": { + "public.customers": { "name": "customers", - "schema": "cms-payload", + "schema": "", "columns": { "id": { "name": "id", @@ -772,14 +764,14 @@ "company_size": { "name": "company_size", "type": "enum_customers_company_size", - "typeSchema": "cms-payload", + "typeSchema": "public", "primaryKey": false, "notNull": false }, "region": { "name": "region", "type": "enum_customers_region", - "typeSchema": "cms-payload", + "typeSchema": "public", "primaryKey": false, "notNull": false }, @@ -836,7 +828,7 @@ "_status": { "name": "_status", "type": "enum_customers_status", - "typeSchema": "cms-payload", + "typeSchema": "public", "primaryKey": false, "notNull": false, "default": "'draft'" @@ -954,7 +946,6 @@ "name": "customers_meta_image_id_media_id_fk", "tableFrom": "customers", "tableTo": "media", - "schemaTo": "cms-payload", "columnsFrom": [ "meta_image_id" ], @@ -968,7 +959,6 @@ "name": "customers_logo_id_media_id_fk", "tableFrom": "customers", "tableTo": "media", - "schemaTo": "cms-payload", "columnsFrom": [ "logo_id" ], @@ -982,7 +972,6 @@ "name": "customers_logo_inverse_id_media_id_fk", "tableFrom": "customers", "tableTo": "media", - "schemaTo": "cms-payload", "columnsFrom": [ "logo_inverse_id" ], @@ -999,9 +988,9 @@ "checkConstraints": {}, "isRLSEnabled": false }, - "cms-payload._customers_v_version_stats": { + "public._customers_v_version_stats": { "name": "_customers_v_version_stats", - "schema": "cms-payload", + "schema": "", "columns": { "_order": { "name": "_order", @@ -1077,7 +1066,6 @@ "name": "_customers_v_version_stats_parent_id_fk", "tableFrom": "_customers_v_version_stats", "tableTo": "_customers_v", - "schemaTo": "cms-payload", "columnsFrom": [ "_parent_id" ], @@ -1094,9 +1082,9 @@ "checkConstraints": {}, "isRLSEnabled": false }, - "cms-payload._customers_v_version_misc": { + "public._customers_v_version_misc": { "name": "_customers_v_version_misc", - "schema": "cms-payload", + "schema": "", "columns": { "_order": { "name": "_order", @@ -1172,7 +1160,6 @@ "name": "_customers_v_version_misc_parent_id_fk", "tableFrom": "_customers_v_version_misc", "tableTo": "_customers_v", - "schemaTo": "cms-payload", "columnsFrom": [ "_parent_id" ], @@ -1189,9 +1176,9 @@ "checkConstraints": {}, "isRLSEnabled": false }, - "cms-payload._customers_v_version_industry": { + "public._customers_v_version_industry": { "name": "_customers_v_version_industry", - "schema": "cms-payload", + "schema": "", "columns": { "order": { "name": "order", @@ -1208,7 +1195,7 @@ "value": { "name": "value", "type": "enum__customers_v_version_industry", - "typeSchema": "cms-payload", + "typeSchema": "public", "primaryKey": false, "notNull": false }, @@ -1256,7 +1243,6 @@ "name": "_customers_v_version_industry_parent_fk", "tableFrom": "_customers_v_version_industry", "tableTo": "_customers_v", - "schemaTo": "cms-payload", "columnsFrom": [ "parent_id" ], @@ -1273,9 +1259,9 @@ "checkConstraints": {}, "isRLSEnabled": false }, - "cms-payload._customers_v_version_supabase_products": { + "public._customers_v_version_supabase_products": { "name": "_customers_v_version_supabase_products", - "schema": "cms-payload", + "schema": "", "columns": { "order": { "name": "order", @@ -1292,7 +1278,7 @@ "value": { "name": "value", "type": "enum__customers_v_version_supabase_products", - "typeSchema": "cms-payload", + "typeSchema": "public", "primaryKey": false, "notNull": false }, @@ -1340,7 +1326,6 @@ "name": "_customers_v_version_supabase_products_parent_fk", "tableFrom": "_customers_v_version_supabase_products", "tableTo": "_customers_v", - "schemaTo": "cms-payload", "columnsFrom": [ "parent_id" ], @@ -1357,9 +1342,9 @@ "checkConstraints": {}, "isRLSEnabled": false }, - "cms-payload._customers_v": { + "public._customers_v": { "name": "_customers_v", - "schema": "cms-payload", + "schema": "", "columns": { "id": { "name": "id", @@ -1425,14 +1410,14 @@ "version_company_size": { "name": "version_company_size", "type": "enum__customers_v_version_company_size", - "typeSchema": "cms-payload", + "typeSchema": "public", "primaryKey": false, "notNull": false }, "version_region": { "name": "version_region", "type": "enum__customers_v_version_region", - "typeSchema": "cms-payload", + "typeSchema": "public", "primaryKey": false, "notNull": false }, @@ -1487,7 +1472,7 @@ "version__status": { "name": "version__status", "type": "enum__customers_v_version_status", - "typeSchema": "cms-payload", + "typeSchema": "public", "primaryKey": false, "notNull": false, "default": "'draft'" @@ -1706,7 +1691,6 @@ "name": "_customers_v_parent_id_customers_id_fk", "tableFrom": "_customers_v", "tableTo": "customers", - "schemaTo": "cms-payload", "columnsFrom": [ "parent_id" ], @@ -1720,7 +1704,6 @@ "name": "_customers_v_version_meta_image_id_media_id_fk", "tableFrom": "_customers_v", "tableTo": "media", - "schemaTo": "cms-payload", "columnsFrom": [ "version_meta_image_id" ], @@ -1734,7 +1717,6 @@ "name": "_customers_v_version_logo_id_media_id_fk", "tableFrom": "_customers_v", "tableTo": "media", - "schemaTo": "cms-payload", "columnsFrom": [ "version_logo_id" ], @@ -1748,7 +1730,6 @@ "name": "_customers_v_version_logo_inverse_id_media_id_fk", "tableFrom": "_customers_v", "tableTo": "media", - "schemaTo": "cms-payload", "columnsFrom": [ "version_logo_inverse_id" ], @@ -1765,9 +1746,9 @@ "checkConstraints": {}, "isRLSEnabled": false }, - "cms-payload.events_type": { + "public.events_type": { "name": "events_type", - "schema": "cms-payload", + "schema": "", "columns": { "order": { "name": "order", @@ -1784,7 +1765,7 @@ "value": { "name": "value", "type": "enum_events_type", - "typeSchema": "cms-payload", + "typeSchema": "public", "primaryKey": false, "notNull": false }, @@ -1832,7 +1813,6 @@ "name": "events_type_parent_fk", "tableFrom": "events_type", "tableTo": "events", - "schemaTo": "cms-payload", "columnsFrom": [ "parent_id" ], @@ -1849,9 +1829,9 @@ "checkConstraints": {}, "isRLSEnabled": false }, - "cms-payload.events": { + "public.events": { "name": "events", - "schema": "cms-payload", + "schema": "", "columns": { "id": { "name": "id", @@ -1911,7 +1891,7 @@ "timezone": { "name": "timezone", "type": "enum_events_timezone", - "typeSchema": "cms-payload", + "typeSchema": "public", "primaryKey": false, "notNull": false }, @@ -1957,7 +1937,7 @@ "link_target": { "name": "link_target", "type": "enum_events_link_target", - "typeSchema": "cms-payload", + "typeSchema": "public", "primaryKey": false, "notNull": false, "default": "'_blank'" @@ -1971,7 +1951,7 @@ "main_cta_target": { "name": "main_cta_target", "type": "enum_events_main_cta_target", - "typeSchema": "cms-payload", + "typeSchema": "public", "primaryKey": false, "notNull": false, "default": "'_blank'" @@ -2074,7 +2054,7 @@ "_status": { "name": "_status", "type": "enum_events_status", - "typeSchema": "cms-payload", + "typeSchema": "public", "primaryKey": false, "notNull": false, "default": "'draft'" @@ -2222,7 +2202,6 @@ "name": "events_thumb_id_media_id_fk", "tableFrom": "events", "tableTo": "media", - "schemaTo": "cms-payload", "columnsFrom": [ "thumb_id" ], @@ -2236,7 +2215,6 @@ "name": "events_image_id_media_id_fk", "tableFrom": "events", "tableTo": "media", - "schemaTo": "cms-payload", "columnsFrom": [ "image_id" ], @@ -2250,7 +2228,6 @@ "name": "events_company_logo_id_media_id_fk", "tableFrom": "events", "tableTo": "media", - "schemaTo": "cms-payload", "columnsFrom": [ "company_logo_id" ], @@ -2264,7 +2241,6 @@ "name": "events_company_logo_light_id_media_id_fk", "tableFrom": "events", "tableTo": "media", - "schemaTo": "cms-payload", "columnsFrom": [ "company_logo_light_id" ], @@ -2278,7 +2254,6 @@ "name": "events_meta_image_id_media_id_fk", "tableFrom": "events", "tableTo": "media", - "schemaTo": "cms-payload", "columnsFrom": [ "meta_image_id" ], @@ -2295,9 +2270,9 @@ "checkConstraints": {}, "isRLSEnabled": false }, - "cms-payload.events_rels": { + "public.events_rels": { "name": "events_rels", - "schema": "cms-payload", + "schema": "", "columns": { "id": { "name": "id", @@ -2397,7 +2372,6 @@ "name": "events_rels_parent_fk", "tableFrom": "events_rels", "tableTo": "events", - "schemaTo": "cms-payload", "columnsFrom": [ "parent_id" ], @@ -2411,7 +2385,6 @@ "name": "events_rels_authors_fk", "tableFrom": "events_rels", "tableTo": "authors", - "schemaTo": "cms-payload", "columnsFrom": [ "authors_id" ], @@ -2428,9 +2401,9 @@ "checkConstraints": {}, "isRLSEnabled": false }, - "cms-payload._events_v_version_type": { + "public._events_v_version_type": { "name": "_events_v_version_type", - "schema": "cms-payload", + "schema": "", "columns": { "order": { "name": "order", @@ -2447,7 +2420,7 @@ "value": { "name": "value", "type": "enum__events_v_version_type", - "typeSchema": "cms-payload", + "typeSchema": "public", "primaryKey": false, "notNull": false }, @@ -2495,7 +2468,6 @@ "name": "_events_v_version_type_parent_fk", "tableFrom": "_events_v_version_type", "tableTo": "_events_v", - "schemaTo": "cms-payload", "columnsFrom": [ "parent_id" ], @@ -2512,9 +2484,9 @@ "checkConstraints": {}, "isRLSEnabled": false }, - "cms-payload._events_v": { + "public._events_v": { "name": "_events_v", - "schema": "cms-payload", + "schema": "", "columns": { "id": { "name": "id", @@ -2580,7 +2552,7 @@ "version_timezone": { "name": "version_timezone", "type": "enum__events_v_version_timezone", - "typeSchema": "cms-payload", + "typeSchema": "public", "primaryKey": false, "notNull": false }, @@ -2626,7 +2598,7 @@ "version_link_target": { "name": "version_link_target", "type": "enum__events_v_version_link_target", - "typeSchema": "cms-payload", + "typeSchema": "public", "primaryKey": false, "notNull": false, "default": "'_blank'" @@ -2640,7 +2612,7 @@ "version_main_cta_target": { "name": "version_main_cta_target", "type": "enum__events_v_version_main_cta_target", - "typeSchema": "cms-payload", + "typeSchema": "public", "primaryKey": false, "notNull": false, "default": "'_blank'" @@ -2741,7 +2713,7 @@ "version__status": { "name": "version__status", "type": "enum__events_v_version_status", - "typeSchema": "cms-payload", + "typeSchema": "public", "primaryKey": false, "notNull": false, "default": "'draft'" @@ -2990,7 +2962,6 @@ "name": "_events_v_parent_id_events_id_fk", "tableFrom": "_events_v", "tableTo": "events", - "schemaTo": "cms-payload", "columnsFrom": [ "parent_id" ], @@ -3004,7 +2975,6 @@ "name": "_events_v_version_thumb_id_media_id_fk", "tableFrom": "_events_v", "tableTo": "media", - "schemaTo": "cms-payload", "columnsFrom": [ "version_thumb_id" ], @@ -3018,7 +2988,6 @@ "name": "_events_v_version_image_id_media_id_fk", "tableFrom": "_events_v", "tableTo": "media", - "schemaTo": "cms-payload", "columnsFrom": [ "version_image_id" ], @@ -3032,7 +3001,6 @@ "name": "_events_v_version_company_logo_id_media_id_fk", "tableFrom": "_events_v", "tableTo": "media", - "schemaTo": "cms-payload", "columnsFrom": [ "version_company_logo_id" ], @@ -3046,7 +3014,6 @@ "name": "_events_v_version_company_logo_light_id_media_id_fk", "tableFrom": "_events_v", "tableTo": "media", - "schemaTo": "cms-payload", "columnsFrom": [ "version_company_logo_light_id" ], @@ -3060,7 +3027,6 @@ "name": "_events_v_version_meta_image_id_media_id_fk", "tableFrom": "_events_v", "tableTo": "media", - "schemaTo": "cms-payload", "columnsFrom": [ "version_meta_image_id" ], @@ -3077,9 +3043,9 @@ "checkConstraints": {}, "isRLSEnabled": false }, - "cms-payload._events_v_rels": { + "public._events_v_rels": { "name": "_events_v_rels", - "schema": "cms-payload", + "schema": "", "columns": { "id": { "name": "id", @@ -3179,7 +3145,6 @@ "name": "_events_v_rels_parent_fk", "tableFrom": "_events_v_rels", "tableTo": "_events_v", - "schemaTo": "cms-payload", "columnsFrom": [ "parent_id" ], @@ -3193,7 +3158,6 @@ "name": "_events_v_rels_authors_fk", "tableFrom": "_events_v_rels", "tableTo": "authors", - "schemaTo": "cms-payload", "columnsFrom": [ "authors_id" ], @@ -3210,9 +3174,9 @@ "checkConstraints": {}, "isRLSEnabled": false }, - "cms-payload.media": { + "public.media": { "name": "media", - "schema": "cms-payload", + "schema": "", "columns": { "id": { "name": "id", @@ -3719,9 +3683,9 @@ "checkConstraints": {}, "isRLSEnabled": false }, - "cms-payload.posts": { + "public.posts": { "name": "posts", - "schema": "cms-payload", + "schema": "", "columns": { "id": { "name": "id", @@ -3769,7 +3733,7 @@ "launchweek": { "name": "launchweek", "type": "enum_posts_launchweek", - "typeSchema": "cms-payload", + "typeSchema": "public", "primaryKey": false, "notNull": false }, @@ -3839,7 +3803,7 @@ "_status": { "name": "_status", "type": "enum_posts_status", - "typeSchema": "cms-payload", + "typeSchema": "public", "primaryKey": false, "notNull": false, "default": "'draft'" @@ -3957,7 +3921,6 @@ "name": "posts_thumb_id_media_id_fk", "tableFrom": "posts", "tableTo": "media", - "schemaTo": "cms-payload", "columnsFrom": [ "thumb_id" ], @@ -3971,7 +3934,6 @@ "name": "posts_image_id_media_id_fk", "tableFrom": "posts", "tableTo": "media", - "schemaTo": "cms-payload", "columnsFrom": [ "image_id" ], @@ -3985,7 +3947,6 @@ "name": "posts_meta_image_id_media_id_fk", "tableFrom": "posts", "tableTo": "media", - "schemaTo": "cms-payload", "columnsFrom": [ "meta_image_id" ], @@ -4002,9 +3963,9 @@ "checkConstraints": {}, "isRLSEnabled": false }, - "cms-payload.posts_rels": { + "public.posts_rels": { "name": "posts_rels", - "schema": "cms-payload", + "schema": "", "columns": { "id": { "name": "id", @@ -4146,7 +4107,6 @@ "name": "posts_rels_parent_fk", "tableFrom": "posts_rels", "tableTo": "posts", - "schemaTo": "cms-payload", "columnsFrom": [ "parent_id" ], @@ -4160,7 +4120,6 @@ "name": "posts_rels_categories_fk", "tableFrom": "posts_rels", "tableTo": "categories", - "schemaTo": "cms-payload", "columnsFrom": [ "categories_id" ], @@ -4174,7 +4133,6 @@ "name": "posts_rels_authors_fk", "tableFrom": "posts_rels", "tableTo": "authors", - "schemaTo": "cms-payload", "columnsFrom": [ "authors_id" ], @@ -4188,7 +4146,6 @@ "name": "posts_rels_tags_fk", "tableFrom": "posts_rels", "tableTo": "tags", - "schemaTo": "cms-payload", "columnsFrom": [ "tags_id" ], @@ -4205,9 +4162,9 @@ "checkConstraints": {}, "isRLSEnabled": false }, - "cms-payload._posts_v": { + "public._posts_v": { "name": "_posts_v", - "schema": "cms-payload", + "schema": "", "columns": { "id": { "name": "id", @@ -4261,7 +4218,7 @@ "version_launchweek": { "name": "version_launchweek", "type": "enum__posts_v_version_launchweek", - "typeSchema": "cms-payload", + "typeSchema": "public", "primaryKey": false, "notNull": false }, @@ -4329,7 +4286,7 @@ "version__status": { "name": "version__status", "type": "enum__posts_v_version_status", - "typeSchema": "cms-payload", + "typeSchema": "public", "primaryKey": false, "notNull": false, "default": "'draft'" @@ -4548,7 +4505,6 @@ "name": "_posts_v_parent_id_posts_id_fk", "tableFrom": "_posts_v", "tableTo": "posts", - "schemaTo": "cms-payload", "columnsFrom": [ "parent_id" ], @@ -4562,7 +4518,6 @@ "name": "_posts_v_version_thumb_id_media_id_fk", "tableFrom": "_posts_v", "tableTo": "media", - "schemaTo": "cms-payload", "columnsFrom": [ "version_thumb_id" ], @@ -4576,7 +4531,6 @@ "name": "_posts_v_version_image_id_media_id_fk", "tableFrom": "_posts_v", "tableTo": "media", - "schemaTo": "cms-payload", "columnsFrom": [ "version_image_id" ], @@ -4590,7 +4544,6 @@ "name": "_posts_v_version_meta_image_id_media_id_fk", "tableFrom": "_posts_v", "tableTo": "media", - "schemaTo": "cms-payload", "columnsFrom": [ "version_meta_image_id" ], @@ -4607,9 +4560,9 @@ "checkConstraints": {}, "isRLSEnabled": false }, - "cms-payload._posts_v_rels": { + "public._posts_v_rels": { "name": "_posts_v_rels", - "schema": "cms-payload", + "schema": "", "columns": { "id": { "name": "id", @@ -4751,7 +4704,6 @@ "name": "_posts_v_rels_parent_fk", "tableFrom": "_posts_v_rels", "tableTo": "_posts_v", - "schemaTo": "cms-payload", "columnsFrom": [ "parent_id" ], @@ -4765,7 +4717,6 @@ "name": "_posts_v_rels_categories_fk", "tableFrom": "_posts_v_rels", "tableTo": "categories", - "schemaTo": "cms-payload", "columnsFrom": [ "categories_id" ], @@ -4779,7 +4730,6 @@ "name": "_posts_v_rels_authors_fk", "tableFrom": "_posts_v_rels", "tableTo": "authors", - "schemaTo": "cms-payload", "columnsFrom": [ "authors_id" ], @@ -4793,7 +4743,6 @@ "name": "_posts_v_rels_tags_fk", "tableFrom": "_posts_v_rels", "tableTo": "tags", - "schemaTo": "cms-payload", "columnsFrom": [ "tags_id" ], @@ -4810,9 +4759,9 @@ "checkConstraints": {}, "isRLSEnabled": false }, - "cms-payload.tags": { + "public.tags": { "name": "tags", - "schema": "cms-payload", + "schema": "", "columns": { "id": { "name": "id", @@ -4880,9 +4829,9 @@ "checkConstraints": {}, "isRLSEnabled": false }, - "cms-payload.users_roles": { + "public.users_roles": { "name": "users_roles", - "schema": "cms-payload", + "schema": "", "columns": { "order": { "name": "order", @@ -4899,7 +4848,7 @@ "value": { "name": "value", "type": "enum_users_roles", - "typeSchema": "cms-payload", + "typeSchema": "public", "primaryKey": false, "notNull": false }, @@ -4947,7 +4896,6 @@ "name": "users_roles_parent_fk", "tableFrom": "users_roles", "tableTo": "users", - "schemaTo": "cms-payload", "columnsFrom": [ "parent_id" ], @@ -4964,9 +4912,9 @@ "checkConstraints": {}, "isRLSEnabled": false }, - "cms-payload.users": { + "public.users": { "name": "users", - "schema": "cms-payload", + "schema": "", "columns": { "id": { "name": "id", @@ -5086,9 +5034,9 @@ "checkConstraints": {}, "isRLSEnabled": false }, - "cms-payload.payload_jobs_log": { + "public.payload_jobs_log": { "name": "payload_jobs_log", - "schema": "cms-payload", + "schema": "", "columns": { "_order": { "name": "_order", @@ -5123,7 +5071,7 @@ "task_slug": { "name": "task_slug", "type": "enum_payload_jobs_log_task_slug", - "typeSchema": "cms-payload", + "typeSchema": "public", "primaryKey": false, "notNull": true }, @@ -5148,7 +5096,7 @@ "state": { "name": "state", "type": "enum_payload_jobs_log_state", - "typeSchema": "cms-payload", + "typeSchema": "public", "primaryKey": false, "notNull": true }, @@ -5196,7 +5144,6 @@ "name": "payload_jobs_log_parent_id_fk", "tableFrom": "payload_jobs_log", "tableTo": "payload_jobs", - "schemaTo": "cms-payload", "columnsFrom": [ "_parent_id" ], @@ -5213,9 +5160,9 @@ "checkConstraints": {}, "isRLSEnabled": false }, - "cms-payload.payload_jobs": { + "public.payload_jobs": { "name": "payload_jobs", - "schema": "cms-payload", + "schema": "", "columns": { "id": { "name": "id", @@ -5258,7 +5205,7 @@ "task_slug": { "name": "task_slug", "type": "enum_payload_jobs_task_slug", - "typeSchema": "cms-payload", + "typeSchema": "public", "primaryKey": false, "notNull": false }, @@ -5441,9 +5388,9 @@ "checkConstraints": {}, "isRLSEnabled": false }, - "cms-payload.payload_locked_documents": { + "public.payload_locked_documents": { "name": "payload_locked_documents", - "schema": "cms-payload", + "schema": "", "columns": { "id": { "name": "id", @@ -5526,9 +5473,9 @@ "checkConstraints": {}, "isRLSEnabled": false }, - "cms-payload.payload_locked_documents_rels": { + "public.payload_locked_documents_rels": { "name": "payload_locked_documents_rels", - "schema": "cms-payload", + "schema": "", "columns": { "id": { "name": "id", @@ -5796,7 +5743,6 @@ "name": "payload_locked_documents_rels_parent_fk", "tableFrom": "payload_locked_documents_rels", "tableTo": "payload_locked_documents", - "schemaTo": "cms-payload", "columnsFrom": [ "parent_id" ], @@ -5810,7 +5756,6 @@ "name": "payload_locked_documents_rels_authors_fk", "tableFrom": "payload_locked_documents_rels", "tableTo": "authors", - "schemaTo": "cms-payload", "columnsFrom": [ "authors_id" ], @@ -5824,7 +5769,6 @@ "name": "payload_locked_documents_rels_categories_fk", "tableFrom": "payload_locked_documents_rels", "tableTo": "categories", - "schemaTo": "cms-payload", "columnsFrom": [ "categories_id" ], @@ -5838,7 +5782,6 @@ "name": "payload_locked_documents_rels_customers_fk", "tableFrom": "payload_locked_documents_rels", "tableTo": "customers", - "schemaTo": "cms-payload", "columnsFrom": [ "customers_id" ], @@ -5852,7 +5795,6 @@ "name": "payload_locked_documents_rels_events_fk", "tableFrom": "payload_locked_documents_rels", "tableTo": "events", - "schemaTo": "cms-payload", "columnsFrom": [ "events_id" ], @@ -5866,7 +5808,6 @@ "name": "payload_locked_documents_rels_media_fk", "tableFrom": "payload_locked_documents_rels", "tableTo": "media", - "schemaTo": "cms-payload", "columnsFrom": [ "media_id" ], @@ -5880,7 +5821,6 @@ "name": "payload_locked_documents_rels_posts_fk", "tableFrom": "payload_locked_documents_rels", "tableTo": "posts", - "schemaTo": "cms-payload", "columnsFrom": [ "posts_id" ], @@ -5894,7 +5834,6 @@ "name": "payload_locked_documents_rels_tags_fk", "tableFrom": "payload_locked_documents_rels", "tableTo": "tags", - "schemaTo": "cms-payload", "columnsFrom": [ "tags_id" ], @@ -5908,7 +5847,6 @@ "name": "payload_locked_documents_rels_users_fk", "tableFrom": "payload_locked_documents_rels", "tableTo": "users", - "schemaTo": "cms-payload", "columnsFrom": [ "users_id" ], @@ -5922,7 +5860,6 @@ "name": "payload_locked_documents_rels_payload_jobs_fk", "tableFrom": "payload_locked_documents_rels", "tableTo": "payload_jobs", - "schemaTo": "cms-payload", "columnsFrom": [ "payload_jobs_id" ], @@ -5939,9 +5876,9 @@ "checkConstraints": {}, "isRLSEnabled": false }, - "cms-payload.payload_preferences": { + "public.payload_preferences": { "name": "payload_preferences", - "schema": "cms-payload", + "schema": "", "columns": { "id": { "name": "id", @@ -6030,9 +5967,9 @@ "checkConstraints": {}, "isRLSEnabled": false }, - "cms-payload.payload_preferences_rels": { + "public.payload_preferences_rels": { "name": "payload_preferences_rels", - "schema": "cms-payload", + "schema": "", "columns": { "id": { "name": "id", @@ -6132,7 +6069,6 @@ "name": "payload_preferences_rels_parent_fk", "tableFrom": "payload_preferences_rels", "tableTo": "payload_preferences", - "schemaTo": "cms-payload", "columnsFrom": [ "parent_id" ], @@ -6146,7 +6082,6 @@ "name": "payload_preferences_rels_users_fk", "tableFrom": "payload_preferences_rels", "tableTo": "users", - "schemaTo": "cms-payload", "columnsFrom": [ "users_id" ], @@ -6163,9 +6098,9 @@ "checkConstraints": {}, "isRLSEnabled": false }, - "cms-payload.payload_migrations": { + "public.payload_migrations": { "name": "payload_migrations", - "schema": "cms-payload", + "schema": "", "columns": { "id": { "name": "id", @@ -6241,9 +6176,9 @@ } }, "enums": { - "cms-payload.enum_customers_industry": { + "public.enum_customers_industry": { "name": "enum_customers_industry", - "schema": "cms-payload", + "schema": "public", "values": [ "healthcare", "fintech", @@ -6259,9 +6194,9 @@ "developer-tools" ] }, - "cms-payload.enum_customers_supabase_products": { + "public.enum_customers_supabase_products": { "name": "enum_customers_supabase_products", - "schema": "cms-payload", + "schema": "public", "values": [ "database", "auth", @@ -6271,18 +6206,18 @@ "vector" ] }, - "cms-payload.enum_customers_company_size": { + "public.enum_customers_company_size": { "name": "enum_customers_company_size", - "schema": "cms-payload", + "schema": "public", "values": [ "startup", "enterprise", "indie_dev" ] }, - "cms-payload.enum_customers_region": { + "public.enum_customers_region": { "name": "enum_customers_region", - "schema": "cms-payload", + "schema": "public", "values": [ "Asia", "Europe", @@ -6292,17 +6227,17 @@ "Oceania" ] }, - "cms-payload.enum_customers_status": { + "public.enum_customers_status": { "name": "enum_customers_status", - "schema": "cms-payload", + "schema": "public", "values": [ "draft", "published" ] }, - "cms-payload.enum__customers_v_version_industry": { + "public.enum__customers_v_version_industry": { "name": "enum__customers_v_version_industry", - "schema": "cms-payload", + "schema": "public", "values": [ "healthcare", "fintech", @@ -6318,9 +6253,9 @@ "developer-tools" ] }, - "cms-payload.enum__customers_v_version_supabase_products": { + "public.enum__customers_v_version_supabase_products": { "name": "enum__customers_v_version_supabase_products", - "schema": "cms-payload", + "schema": "public", "values": [ "database", "auth", @@ -6330,18 +6265,18 @@ "vector" ] }, - "cms-payload.enum__customers_v_version_company_size": { + "public.enum__customers_v_version_company_size": { "name": "enum__customers_v_version_company_size", - "schema": "cms-payload", + "schema": "public", "values": [ "startup", "enterprise", "indie_dev" ] }, - "cms-payload.enum__customers_v_version_region": { + "public.enum__customers_v_version_region": { "name": "enum__customers_v_version_region", - "schema": "cms-payload", + "schema": "public", "values": [ "Asia", "Europe", @@ -6351,17 +6286,17 @@ "Oceania" ] }, - "cms-payload.enum__customers_v_version_status": { + "public.enum__customers_v_version_status": { "name": "enum__customers_v_version_status", - "schema": "cms-payload", + "schema": "public", "values": [ "draft", "published" ] }, - "cms-payload.enum_events_type": { + "public.enum_events_type": { "name": "enum_events_type", - "schema": "cms-payload", + "schema": "public", "values": [ "conference", "hackathon", @@ -6374,9 +6309,9 @@ "other" ] }, - "cms-payload.enum_events_timezone": { + "public.enum_events_timezone": { "name": "enum_events_timezone", - "schema": "cms-payload", + "schema": "public", "values": [ "Africa/Abidjan", "Africa/Accra", @@ -6807,33 +6742,33 @@ "UTC" ] }, - "cms-payload.enum_events_link_target": { + "public.enum_events_link_target": { "name": "enum_events_link_target", - "schema": "cms-payload", + "schema": "public", "values": [ "_self", "_blank" ] }, - "cms-payload.enum_events_main_cta_target": { + "public.enum_events_main_cta_target": { "name": "enum_events_main_cta_target", - "schema": "cms-payload", + "schema": "public", "values": [ "_self", "_blank" ] }, - "cms-payload.enum_events_status": { + "public.enum_events_status": { "name": "enum_events_status", - "schema": "cms-payload", + "schema": "public", "values": [ "draft", "published" ] }, - "cms-payload.enum__events_v_version_type": { + "public.enum__events_v_version_type": { "name": "enum__events_v_version_type", - "schema": "cms-payload", + "schema": "public", "values": [ "conference", "hackathon", @@ -6846,9 +6781,9 @@ "other" ] }, - "cms-payload.enum__events_v_version_timezone": { + "public.enum__events_v_version_timezone": { "name": "enum__events_v_version_timezone", - "schema": "cms-payload", + "schema": "public", "values": [ "Africa/Abidjan", "Africa/Accra", @@ -7279,33 +7214,33 @@ "UTC" ] }, - "cms-payload.enum__events_v_version_link_target": { + "public.enum__events_v_version_link_target": { "name": "enum__events_v_version_link_target", - "schema": "cms-payload", + "schema": "public", "values": [ "_self", "_blank" ] }, - "cms-payload.enum__events_v_version_main_cta_target": { + "public.enum__events_v_version_main_cta_target": { "name": "enum__events_v_version_main_cta_target", - "schema": "cms-payload", + "schema": "public", "values": [ "_self", "_blank" ] }, - "cms-payload.enum__events_v_version_status": { + "public.enum__events_v_version_status": { "name": "enum__events_v_version_status", - "schema": "cms-payload", + "schema": "public", "values": [ "draft", "published" ] }, - "cms-payload.enum_posts_launchweek": { + "public.enum_posts_launchweek": { "name": "enum_posts_launchweek", - "schema": "cms-payload", + "schema": "public", "values": [ "6", "7", @@ -7318,17 +7253,17 @@ "15" ] }, - "cms-payload.enum_posts_status": { + "public.enum_posts_status": { "name": "enum_posts_status", - "schema": "cms-payload", + "schema": "public", "values": [ "draft", "published" ] }, - "cms-payload.enum__posts_v_version_launchweek": { + "public.enum__posts_v_version_launchweek": { "name": "enum__posts_v_version_launchweek", - "schema": "cms-payload", + "schema": "public", "values": [ "6", "7", @@ -7341,50 +7276,48 @@ "15" ] }, - "cms-payload.enum__posts_v_version_status": { + "public.enum__posts_v_version_status": { "name": "enum__posts_v_version_status", - "schema": "cms-payload", + "schema": "public", "values": [ "draft", "published" ] }, - "cms-payload.enum_users_roles": { + "public.enum_users_roles": { "name": "enum_users_roles", - "schema": "cms-payload", + "schema": "public", "values": [ "admin", "editor" ] }, - "cms-payload.enum_payload_jobs_log_task_slug": { + "public.enum_payload_jobs_log_task_slug": { "name": "enum_payload_jobs_log_task_slug", - "schema": "cms-payload", + "schema": "public", "values": [ "inline", "schedulePublish" ] }, - "cms-payload.enum_payload_jobs_log_state": { + "public.enum_payload_jobs_log_state": { "name": "enum_payload_jobs_log_state", - "schema": "cms-payload", + "schema": "public", "values": [ "failed", "succeeded" ] }, - "cms-payload.enum_payload_jobs_task_slug": { + "public.enum_payload_jobs_task_slug": { "name": "enum_payload_jobs_task_slug", - "schema": "cms-payload", + "schema": "public", "values": [ "inline", "schedulePublish" ] } }, - "schemas": { - "cms-payload": "cms-payload" - }, + "schemas": {}, "sequences": {}, "roles": {}, "policies": {}, diff --git a/apps/cms/src/migrations/20250619_155345.ts b/apps/cms/src/migrations/20250619_155345.ts new file mode 100644 index 00000000000..65af423d602 --- /dev/null +++ b/apps/cms/src/migrations/20250619_155345.ts @@ -0,0 +1,1136 @@ +import { MigrateUpArgs, MigrateDownArgs, sql } from '@payloadcms/db-postgres' + +export async function up({ db, payload, req }: MigrateUpArgs): Promise { + await db.execute(sql` + CREATE TYPE "public"."enum_customers_industry" AS ENUM('healthcare', 'fintech', 'ecommerce', 'education', 'gaming', 'media', 'real-estate', 'saas', 'social', 'analytics', 'ai', 'developer-tools'); + CREATE TYPE "public"."enum_customers_supabase_products" AS ENUM('database', 'auth', 'storage', 'realtime', 'functions', 'vector'); + CREATE TYPE "public"."enum_customers_company_size" AS ENUM('startup', 'enterprise', 'indie_dev'); + CREATE TYPE "public"."enum_customers_region" AS ENUM('Asia', 'Europe', 'North America', 'South America', 'Africa', 'Oceania'); + CREATE TYPE "public"."enum_customers_status" AS ENUM('draft', 'published'); + CREATE TYPE "public"."enum__customers_v_version_industry" AS ENUM('healthcare', 'fintech', 'ecommerce', 'education', 'gaming', 'media', 'real-estate', 'saas', 'social', 'analytics', 'ai', 'developer-tools'); + CREATE TYPE "public"."enum__customers_v_version_supabase_products" AS ENUM('database', 'auth', 'storage', 'realtime', 'functions', 'vector'); + CREATE TYPE "public"."enum__customers_v_version_company_size" AS ENUM('startup', 'enterprise', 'indie_dev'); + CREATE TYPE "public"."enum__customers_v_version_region" AS ENUM('Asia', 'Europe', 'North America', 'South America', 'Africa', 'Oceania'); + CREATE TYPE "public"."enum__customers_v_version_status" AS ENUM('draft', 'published'); + CREATE TYPE "public"."enum_events_type" AS ENUM('conference', 'hackathon', 'launch-week', 'meetup', 'office-hours', 'talk', 'webinar', 'workshop', 'other'); + CREATE TYPE "public"."enum_events_timezone" AS ENUM('Africa/Abidjan', 'Africa/Accra', 'Africa/Addis_Ababa', 'Africa/Algiers', 'Africa/Asmara', 'Africa/Bamako', 'Africa/Bangui', 'Africa/Banjul', 'Africa/Bissau', 'Africa/Blantyre', 'Africa/Brazzaville', 'Africa/Bujumbura', 'Africa/Cairo', 'Africa/Casablanca', 'Africa/Ceuta', 'Africa/Conakry', 'Africa/Dakar', 'Africa/Dar_es_Salaam', 'Africa/Djibouti', 'Africa/Douala', 'Africa/El_Aaiun', 'Africa/Freetown', 'Africa/Gaborone', 'Africa/Harare', 'Africa/Johannesburg', 'Africa/Juba', 'Africa/Kampala', 'Africa/Khartoum', 'Africa/Kigali', 'Africa/Kinshasa', 'Africa/Lagos', 'Africa/Libreville', 'Africa/Lome', 'Africa/Luanda', 'Africa/Lubumbashi', 'Africa/Lusaka', 'Africa/Malabo', 'Africa/Maputo', 'Africa/Maseru', 'Africa/Mbabane', 'Africa/Mogadishu', 'Africa/Monrovia', 'Africa/Nairobi', 'Africa/Ndjamena', 'Africa/Niamey', 'Africa/Nouakchott', 'Africa/Ouagadougou', 'Africa/Porto-Novo', 'Africa/Sao_Tome', 'Africa/Tripoli', 'Africa/Tunis', 'Africa/Windhoek', 'America/Adak', 'America/Anchorage', 'America/Anguilla', 'America/Antigua', 'America/Araguaina', 'America/Argentina/Buenos_Aires', 'America/Argentina/Catamarca', 'America/Argentina/Cordoba', 'America/Argentina/Jujuy', 'America/Argentina/La_Rioja', 'America/Argentina/Mendoza', 'America/Argentina/Rio_Gallegos', 'America/Argentina/Salta', 'America/Argentina/San_Juan', 'America/Argentina/San_Luis', 'America/Argentina/Tucuman', 'America/Argentina/Ushuaia', 'America/Aruba', 'America/Asuncion', 'America/Atikokan', 'America/Bahia', 'America/Bahia_Banderas', 'America/Barbados', 'America/Belem', 'America/Belize', 'America/Blanc-Sablon', 'America/Boa_Vista', 'America/Bogota', 'America/Boise', 'America/Cambridge_Bay', 'America/Campo_Grande', 'America/Cancun', 'America/Caracas', 'America/Cayenne', 'America/Cayman', 'America/Chicago', 'America/Chihuahua', 'America/Costa_Rica', 'America/Creston', 'America/Cuiaba', 'America/Curacao', 'America/Danmarkshavn', 'America/Dawson', 'America/Dawson_Creek', 'America/Denver', 'America/Detroit', 'America/Dominica', 'America/Edmonton', 'America/Eirunepe', 'America/El_Salvador', 'America/Fort_Nelson', 'America/Fortaleza', 'America/Glace_Bay', 'America/Godthab', 'America/Goose_Bay', 'America/Grand_Turk', 'America/Grenada', 'America/Guadeloupe', 'America/Guatemala', 'America/Guayaquil', 'America/Guyana', 'America/Halifax', 'America/Havana', 'America/Hermosillo', 'America/Indiana/Indianapolis', 'America/Indiana/Knox', 'America/Indiana/Marengo', 'America/Indiana/Petersburg', 'America/Indiana/Tell_City', 'America/Indiana/Vevay', 'America/Indiana/Vincennes', 'America/Indiana/Winamac', 'America/Inuvik', 'America/Iqaluit', 'America/Jamaica', 'America/Juneau', 'America/Kentucky/Louisville', 'America/Kentucky/Monticello', 'America/Kralendijk', 'America/La_Paz', 'America/Lima', 'America/Los_Angeles', 'America/Lower_Princes', 'America/Maceio', 'America/Managua', 'America/Manaus', 'America/Marigot', 'America/Martinique', 'America/Matamoros', 'America/Mazatlan', 'America/Menominee', 'America/Merida', 'America/Metlakatla', 'America/Mexico_City', 'America/Miquelon', 'America/Moncton', 'America/Monterrey', 'America/Montevideo', 'America/Montserrat', 'America/Nassau', 'America/New_York', 'America/Nipigon', 'America/Nome', 'America/Noronha', 'America/North_Dakota/Beulah', 'America/North_Dakota/Center', 'America/North_Dakota/New_Salem', 'America/Ojinaga', 'America/Panama', 'America/Pangnirtung', 'America/Paramaribo', 'America/Phoenix', 'America/Port-au-Prince', 'America/Port_of_Spain', 'America/Porto_Velho', 'America/Puerto_Rico', 'America/Punta_Arenas', 'America/Rainy_River', 'America/Rankin_Inlet', 'America/Recife', 'America/Regina', 'America/Resolute', 'America/Rio_Branco', 'America/Santarem', 'America/Santiago', 'America/Santo_Domingo', 'America/Sao_Paulo', 'America/Scoresbysund', 'America/Sitka', 'America/St_Barthelemy', 'America/St_Johns', 'America/St_Kitts', 'America/St_Lucia', 'America/St_Thomas', 'America/St_Vincent', 'America/Swift_Current', 'America/Tegucigalpa', 'America/Thule', 'America/Thunder_Bay', 'America/Tijuana', 'America/Toronto', 'America/Tortola', 'America/Vancouver', 'America/Whitehorse', 'America/Winnipeg', 'America/Yakutat', 'America/Yellowknife', 'Antarctica/Casey', 'Antarctica/Davis', 'Antarctica/DumontDUrville', 'Antarctica/Macquarie', 'Antarctica/Mawson', 'Antarctica/McMurdo', 'Antarctica/Palmer', 'Antarctica/Rothera', 'Antarctica/Syowa', 'Antarctica/Troll', 'Antarctica/Vostok', 'Arctic/Longyearbyen', 'Asia/Aden', 'Asia/Almaty', 'Asia/Amman', 'Asia/Anadyr', 'Asia/Aqtau', 'Asia/Aqtobe', 'Asia/Ashgabat', 'Asia/Atyrau', 'Asia/Baghdad', 'Asia/Bahrain', 'Asia/Baku', 'Asia/Bangkok', 'Asia/Barnaul', 'Asia/Beirut', 'Asia/Bishkek', 'Asia/Brunei', 'Asia/Chita', 'Asia/Choibalsan', 'Asia/Colombo', 'Asia/Damascus', 'Asia/Dhaka', 'Asia/Dili', 'Asia/Dubai', 'Asia/Dushanbe', 'Asia/Famagusta', 'Asia/Gaza', 'Asia/Hebron', 'Asia/Ho_Chi_Minh', 'Asia/Hong_Kong', 'Asia/Hovd', 'Asia/Irkutsk', 'Asia/Istanbul', 'Asia/Jakarta', 'Asia/Jayapura', 'Asia/Jerusalem', 'Asia/Kabul', 'Asia/Kamchatka', 'Asia/Karachi', 'Asia/Kathmandu', 'Asia/Khandyga', 'Asia/Kolkata', 'Asia/Krasnoyarsk', 'Asia/Kuala_Lumpur', 'Asia/Kuching', 'Asia/Kuwait', 'Asia/Macau', 'Asia/Magadan', 'Asia/Makassar', 'Asia/Manila', 'Asia/Muscat', 'Asia/Nicosia', 'Asia/Novokuznetsk', 'Asia/Novosibirsk', 'Asia/Omsk', 'Asia/Oral', 'Asia/Phnom_Penh', 'Asia/Pontianak', 'Asia/Pyongyang', 'Asia/Qatar', 'Asia/Qostanay', 'Asia/Qyzylorda', 'Asia/Riyadh', 'Asia/Sakhalin', 'Asia/Samarkand', 'Asia/Seoul', 'Asia/Shanghai', 'Asia/Singapore', 'Asia/Srednekolymsk', 'Asia/Taipei', 'Asia/Tashkent', 'Asia/Tbilisi', 'Asia/Tehran', 'Asia/Thimphu', 'Asia/Tokyo', 'Asia/Tomsk', 'Asia/Ulaanbaatar', 'Asia/Urumqi', 'Asia/Ust-Nera', 'Asia/Vientiane', 'Asia/Vladivostok', 'Asia/Yakutsk', 'Asia/Yangon', 'Asia/Yekaterinburg', 'Asia/Yerevan', 'Atlantic/Azores', 'Atlantic/Bermuda', 'Atlantic/Canary', 'Atlantic/Cape_Verde', 'Atlantic/Faroe', 'Atlantic/Madeira', 'Atlantic/Reykjavik', 'Atlantic/South_Georgia', 'Atlantic/St_Helena', 'Atlantic/Stanley', 'Australia/Adelaide', 'Australia/Brisbane', 'Australia/Broken_Hill', 'Australia/Currie', 'Australia/Darwin', 'Australia/Eucla', 'Australia/Hobart', 'Australia/Lindeman', 'Australia/Lord_Howe', 'Australia/Melbourne', 'Australia/Perth', 'Australia/Sydney', 'Europe/Amsterdam', 'Europe/Andorra', 'Europe/Astrakhan', 'Europe/Athens', 'Europe/Belgrade', 'Europe/Berlin', 'Europe/Bratislava', 'Europe/Brussels', 'Europe/Bucharest', 'Europe/Budapest', 'Europe/Busingen', 'Europe/Chisinau', 'Europe/Copenhagen', 'Europe/Dublin', 'Europe/Gibraltar', 'Europe/Guernsey', 'Europe/Helsinki', 'Europe/Isle_of_Man', 'Europe/Istanbul', 'Europe/Jersey', 'Europe/Kaliningrad', 'Europe/Kiev', 'Europe/Kirov', 'Europe/Lisbon', 'Europe/Ljubljana', 'Europe/London', 'Europe/Luxembourg', 'Europe/Madrid', 'Europe/Malta', 'Europe/Mariehamn', 'Europe/Minsk', 'Europe/Monaco', 'Europe/Moscow', 'Europe/Oslo', 'Europe/Paris', 'Europe/Podgorica', 'Europe/Prague', 'Europe/Riga', 'Europe/Rome', 'Europe/Samara', 'Europe/San_Marino', 'Europe/Sarajevo', 'Europe/Saratov', 'Europe/Simferopol', 'Europe/Skopje', 'Europe/Sofia', 'Europe/Stockholm', 'Europe/Tallinn', 'Europe/Tirane', 'Europe/Ulyanovsk', 'Europe/Uzhgorod', 'Europe/Vaduz', 'Europe/Vatican', 'Europe/Vienna', 'Europe/Vilnius', 'Europe/Volgograd', 'Europe/Warsaw', 'Europe/Zagreb', 'Europe/Zaporozhye', 'Europe/Zurich', 'Indian/Antananarivo', 'Indian/Chagos', 'Indian/Christmas', 'Indian/Cocos', 'Indian/Comoro', 'Indian/Kerguelen', 'Indian/Mahe', 'Indian/Maldives', 'Indian/Mauritius', 'Indian/Mayotte', 'Indian/Reunion', 'Pacific/Apia', 'Pacific/Auckland', 'Pacific/Bougainville', 'Pacific/Chatham', 'Pacific/Chuuk', 'Pacific/Easter', 'Pacific/Efate', 'Pacific/Enderbury', 'Pacific/Fakaofo', 'Pacific/Fiji', 'Pacific/Funafuti', 'Pacific/Galapagos', 'Pacific/Gambier', 'Pacific/Guadalcanal', 'Pacific/Guam', 'Pacific/Honolulu', 'Pacific/Kiritimati', 'Pacific/Kosrae', 'Pacific/Kwajalein', 'Pacific/Majuro', 'Pacific/Marquesas', 'Pacific/Midway', 'Pacific/Nauru', 'Pacific/Niue', 'Pacific/Norfolk', 'Pacific/Noumea', 'Pacific/Pago_Pago', 'Pacific/Palau', 'Pacific/Pitcairn', 'Pacific/Pohnpei', 'Pacific/Port_Moresby', 'Pacific/Rarotonga', 'Pacific/Saipan', 'Pacific/Tahiti', 'Pacific/Tarawa', 'Pacific/Tongatapu', 'Pacific/Wake', 'Pacific/Wallis', 'UTC'); + CREATE TYPE "public"."enum_events_link_target" AS ENUM('_self', '_blank'); + CREATE TYPE "public"."enum_events_main_cta_target" AS ENUM('_self', '_blank'); + CREATE TYPE "public"."enum_events_status" AS ENUM('draft', 'published'); + CREATE TYPE "public"."enum__events_v_version_type" AS ENUM('conference', 'hackathon', 'launch-week', 'meetup', 'office-hours', 'talk', 'webinar', 'workshop', 'other'); + CREATE TYPE "public"."enum__events_v_version_timezone" AS ENUM('Africa/Abidjan', 'Africa/Accra', 'Africa/Addis_Ababa', 'Africa/Algiers', 'Africa/Asmara', 'Africa/Bamako', 'Africa/Bangui', 'Africa/Banjul', 'Africa/Bissau', 'Africa/Blantyre', 'Africa/Brazzaville', 'Africa/Bujumbura', 'Africa/Cairo', 'Africa/Casablanca', 'Africa/Ceuta', 'Africa/Conakry', 'Africa/Dakar', 'Africa/Dar_es_Salaam', 'Africa/Djibouti', 'Africa/Douala', 'Africa/El_Aaiun', 'Africa/Freetown', 'Africa/Gaborone', 'Africa/Harare', 'Africa/Johannesburg', 'Africa/Juba', 'Africa/Kampala', 'Africa/Khartoum', 'Africa/Kigali', 'Africa/Kinshasa', 'Africa/Lagos', 'Africa/Libreville', 'Africa/Lome', 'Africa/Luanda', 'Africa/Lubumbashi', 'Africa/Lusaka', 'Africa/Malabo', 'Africa/Maputo', 'Africa/Maseru', 'Africa/Mbabane', 'Africa/Mogadishu', 'Africa/Monrovia', 'Africa/Nairobi', 'Africa/Ndjamena', 'Africa/Niamey', 'Africa/Nouakchott', 'Africa/Ouagadougou', 'Africa/Porto-Novo', 'Africa/Sao_Tome', 'Africa/Tripoli', 'Africa/Tunis', 'Africa/Windhoek', 'America/Adak', 'America/Anchorage', 'America/Anguilla', 'America/Antigua', 'America/Araguaina', 'America/Argentina/Buenos_Aires', 'America/Argentina/Catamarca', 'America/Argentina/Cordoba', 'America/Argentina/Jujuy', 'America/Argentina/La_Rioja', 'America/Argentina/Mendoza', 'America/Argentina/Rio_Gallegos', 'America/Argentina/Salta', 'America/Argentina/San_Juan', 'America/Argentina/San_Luis', 'America/Argentina/Tucuman', 'America/Argentina/Ushuaia', 'America/Aruba', 'America/Asuncion', 'America/Atikokan', 'America/Bahia', 'America/Bahia_Banderas', 'America/Barbados', 'America/Belem', 'America/Belize', 'America/Blanc-Sablon', 'America/Boa_Vista', 'America/Bogota', 'America/Boise', 'America/Cambridge_Bay', 'America/Campo_Grande', 'America/Cancun', 'America/Caracas', 'America/Cayenne', 'America/Cayman', 'America/Chicago', 'America/Chihuahua', 'America/Costa_Rica', 'America/Creston', 'America/Cuiaba', 'America/Curacao', 'America/Danmarkshavn', 'America/Dawson', 'America/Dawson_Creek', 'America/Denver', 'America/Detroit', 'America/Dominica', 'America/Edmonton', 'America/Eirunepe', 'America/El_Salvador', 'America/Fort_Nelson', 'America/Fortaleza', 'America/Glace_Bay', 'America/Godthab', 'America/Goose_Bay', 'America/Grand_Turk', 'America/Grenada', 'America/Guadeloupe', 'America/Guatemala', 'America/Guayaquil', 'America/Guyana', 'America/Halifax', 'America/Havana', 'America/Hermosillo', 'America/Indiana/Indianapolis', 'America/Indiana/Knox', 'America/Indiana/Marengo', 'America/Indiana/Petersburg', 'America/Indiana/Tell_City', 'America/Indiana/Vevay', 'America/Indiana/Vincennes', 'America/Indiana/Winamac', 'America/Inuvik', 'America/Iqaluit', 'America/Jamaica', 'America/Juneau', 'America/Kentucky/Louisville', 'America/Kentucky/Monticello', 'America/Kralendijk', 'America/La_Paz', 'America/Lima', 'America/Los_Angeles', 'America/Lower_Princes', 'America/Maceio', 'America/Managua', 'America/Manaus', 'America/Marigot', 'America/Martinique', 'America/Matamoros', 'America/Mazatlan', 'America/Menominee', 'America/Merida', 'America/Metlakatla', 'America/Mexico_City', 'America/Miquelon', 'America/Moncton', 'America/Monterrey', 'America/Montevideo', 'America/Montserrat', 'America/Nassau', 'America/New_York', 'America/Nipigon', 'America/Nome', 'America/Noronha', 'America/North_Dakota/Beulah', 'America/North_Dakota/Center', 'America/North_Dakota/New_Salem', 'America/Ojinaga', 'America/Panama', 'America/Pangnirtung', 'America/Paramaribo', 'America/Phoenix', 'America/Port-au-Prince', 'America/Port_of_Spain', 'America/Porto_Velho', 'America/Puerto_Rico', 'America/Punta_Arenas', 'America/Rainy_River', 'America/Rankin_Inlet', 'America/Recife', 'America/Regina', 'America/Resolute', 'America/Rio_Branco', 'America/Santarem', 'America/Santiago', 'America/Santo_Domingo', 'America/Sao_Paulo', 'America/Scoresbysund', 'America/Sitka', 'America/St_Barthelemy', 'America/St_Johns', 'America/St_Kitts', 'America/St_Lucia', 'America/St_Thomas', 'America/St_Vincent', 'America/Swift_Current', 'America/Tegucigalpa', 'America/Thule', 'America/Thunder_Bay', 'America/Tijuana', 'America/Toronto', 'America/Tortola', 'America/Vancouver', 'America/Whitehorse', 'America/Winnipeg', 'America/Yakutat', 'America/Yellowknife', 'Antarctica/Casey', 'Antarctica/Davis', 'Antarctica/DumontDUrville', 'Antarctica/Macquarie', 'Antarctica/Mawson', 'Antarctica/McMurdo', 'Antarctica/Palmer', 'Antarctica/Rothera', 'Antarctica/Syowa', 'Antarctica/Troll', 'Antarctica/Vostok', 'Arctic/Longyearbyen', 'Asia/Aden', 'Asia/Almaty', 'Asia/Amman', 'Asia/Anadyr', 'Asia/Aqtau', 'Asia/Aqtobe', 'Asia/Ashgabat', 'Asia/Atyrau', 'Asia/Baghdad', 'Asia/Bahrain', 'Asia/Baku', 'Asia/Bangkok', 'Asia/Barnaul', 'Asia/Beirut', 'Asia/Bishkek', 'Asia/Brunei', 'Asia/Chita', 'Asia/Choibalsan', 'Asia/Colombo', 'Asia/Damascus', 'Asia/Dhaka', 'Asia/Dili', 'Asia/Dubai', 'Asia/Dushanbe', 'Asia/Famagusta', 'Asia/Gaza', 'Asia/Hebron', 'Asia/Ho_Chi_Minh', 'Asia/Hong_Kong', 'Asia/Hovd', 'Asia/Irkutsk', 'Asia/Istanbul', 'Asia/Jakarta', 'Asia/Jayapura', 'Asia/Jerusalem', 'Asia/Kabul', 'Asia/Kamchatka', 'Asia/Karachi', 'Asia/Kathmandu', 'Asia/Khandyga', 'Asia/Kolkata', 'Asia/Krasnoyarsk', 'Asia/Kuala_Lumpur', 'Asia/Kuching', 'Asia/Kuwait', 'Asia/Macau', 'Asia/Magadan', 'Asia/Makassar', 'Asia/Manila', 'Asia/Muscat', 'Asia/Nicosia', 'Asia/Novokuznetsk', 'Asia/Novosibirsk', 'Asia/Omsk', 'Asia/Oral', 'Asia/Phnom_Penh', 'Asia/Pontianak', 'Asia/Pyongyang', 'Asia/Qatar', 'Asia/Qostanay', 'Asia/Qyzylorda', 'Asia/Riyadh', 'Asia/Sakhalin', 'Asia/Samarkand', 'Asia/Seoul', 'Asia/Shanghai', 'Asia/Singapore', 'Asia/Srednekolymsk', 'Asia/Taipei', 'Asia/Tashkent', 'Asia/Tbilisi', 'Asia/Tehran', 'Asia/Thimphu', 'Asia/Tokyo', 'Asia/Tomsk', 'Asia/Ulaanbaatar', 'Asia/Urumqi', 'Asia/Ust-Nera', 'Asia/Vientiane', 'Asia/Vladivostok', 'Asia/Yakutsk', 'Asia/Yangon', 'Asia/Yekaterinburg', 'Asia/Yerevan', 'Atlantic/Azores', 'Atlantic/Bermuda', 'Atlantic/Canary', 'Atlantic/Cape_Verde', 'Atlantic/Faroe', 'Atlantic/Madeira', 'Atlantic/Reykjavik', 'Atlantic/South_Georgia', 'Atlantic/St_Helena', 'Atlantic/Stanley', 'Australia/Adelaide', 'Australia/Brisbane', 'Australia/Broken_Hill', 'Australia/Currie', 'Australia/Darwin', 'Australia/Eucla', 'Australia/Hobart', 'Australia/Lindeman', 'Australia/Lord_Howe', 'Australia/Melbourne', 'Australia/Perth', 'Australia/Sydney', 'Europe/Amsterdam', 'Europe/Andorra', 'Europe/Astrakhan', 'Europe/Athens', 'Europe/Belgrade', 'Europe/Berlin', 'Europe/Bratislava', 'Europe/Brussels', 'Europe/Bucharest', 'Europe/Budapest', 'Europe/Busingen', 'Europe/Chisinau', 'Europe/Copenhagen', 'Europe/Dublin', 'Europe/Gibraltar', 'Europe/Guernsey', 'Europe/Helsinki', 'Europe/Isle_of_Man', 'Europe/Istanbul', 'Europe/Jersey', 'Europe/Kaliningrad', 'Europe/Kiev', 'Europe/Kirov', 'Europe/Lisbon', 'Europe/Ljubljana', 'Europe/London', 'Europe/Luxembourg', 'Europe/Madrid', 'Europe/Malta', 'Europe/Mariehamn', 'Europe/Minsk', 'Europe/Monaco', 'Europe/Moscow', 'Europe/Oslo', 'Europe/Paris', 'Europe/Podgorica', 'Europe/Prague', 'Europe/Riga', 'Europe/Rome', 'Europe/Samara', 'Europe/San_Marino', 'Europe/Sarajevo', 'Europe/Saratov', 'Europe/Simferopol', 'Europe/Skopje', 'Europe/Sofia', 'Europe/Stockholm', 'Europe/Tallinn', 'Europe/Tirane', 'Europe/Ulyanovsk', 'Europe/Uzhgorod', 'Europe/Vaduz', 'Europe/Vatican', 'Europe/Vienna', 'Europe/Vilnius', 'Europe/Volgograd', 'Europe/Warsaw', 'Europe/Zagreb', 'Europe/Zaporozhye', 'Europe/Zurich', 'Indian/Antananarivo', 'Indian/Chagos', 'Indian/Christmas', 'Indian/Cocos', 'Indian/Comoro', 'Indian/Kerguelen', 'Indian/Mahe', 'Indian/Maldives', 'Indian/Mauritius', 'Indian/Mayotte', 'Indian/Reunion', 'Pacific/Apia', 'Pacific/Auckland', 'Pacific/Bougainville', 'Pacific/Chatham', 'Pacific/Chuuk', 'Pacific/Easter', 'Pacific/Efate', 'Pacific/Enderbury', 'Pacific/Fakaofo', 'Pacific/Fiji', 'Pacific/Funafuti', 'Pacific/Galapagos', 'Pacific/Gambier', 'Pacific/Guadalcanal', 'Pacific/Guam', 'Pacific/Honolulu', 'Pacific/Kiritimati', 'Pacific/Kosrae', 'Pacific/Kwajalein', 'Pacific/Majuro', 'Pacific/Marquesas', 'Pacific/Midway', 'Pacific/Nauru', 'Pacific/Niue', 'Pacific/Norfolk', 'Pacific/Noumea', 'Pacific/Pago_Pago', 'Pacific/Palau', 'Pacific/Pitcairn', 'Pacific/Pohnpei', 'Pacific/Port_Moresby', 'Pacific/Rarotonga', 'Pacific/Saipan', 'Pacific/Tahiti', 'Pacific/Tarawa', 'Pacific/Tongatapu', 'Pacific/Wake', 'Pacific/Wallis', 'UTC'); + CREATE TYPE "public"."enum__events_v_version_link_target" AS ENUM('_self', '_blank'); + CREATE TYPE "public"."enum__events_v_version_main_cta_target" AS ENUM('_self', '_blank'); + CREATE TYPE "public"."enum__events_v_version_status" AS ENUM('draft', 'published'); + CREATE TYPE "public"."enum_posts_launchweek" AS ENUM('6', '7', '8', 'x', 'ga', '12', '13', '14', '15'); + CREATE TYPE "public"."enum_posts_status" AS ENUM('draft', 'published'); + CREATE TYPE "public"."enum__posts_v_version_launchweek" AS ENUM('6', '7', '8', 'x', 'ga', '12', '13', '14', '15'); + CREATE TYPE "public"."enum__posts_v_version_status" AS ENUM('draft', 'published'); + CREATE TYPE "public"."enum_users_roles" AS ENUM('admin', 'editor'); + CREATE TYPE "public"."enum_payload_jobs_log_task_slug" AS ENUM('inline', 'schedulePublish'); + CREATE TYPE "public"."enum_payload_jobs_log_state" AS ENUM('failed', 'succeeded'); + CREATE TYPE "public"."enum_payload_jobs_task_slug" AS ENUM('inline', 'schedulePublish'); + CREATE TABLE IF NOT EXISTS "authors" ( + "id" serial PRIMARY KEY NOT NULL, + "author" varchar NOT NULL, + "author_id" varchar, + "position" varchar, + "author_url" varchar, + "author_image_url_id" integer, + "username" varchar, + "updated_at" timestamp(3) with time zone DEFAULT now() NOT NULL, + "created_at" timestamp(3) with time zone DEFAULT now() NOT NULL + ); + + CREATE TABLE IF NOT EXISTS "categories_breadcrumbs" ( + "_order" integer NOT NULL, + "_parent_id" integer NOT NULL, + "id" varchar PRIMARY KEY NOT NULL, + "doc_id" integer, + "url" varchar, + "label" varchar + ); + + CREATE TABLE IF NOT EXISTS "categories" ( + "id" serial PRIMARY KEY NOT NULL, + "name" varchar NOT NULL, + "parent_id" integer, + "updated_at" timestamp(3) with time zone DEFAULT now() NOT NULL, + "created_at" timestamp(3) with time zone DEFAULT now() NOT NULL + ); + + CREATE TABLE IF NOT EXISTS "customers_stats" ( + "_order" integer NOT NULL, + "_parent_id" integer NOT NULL, + "id" varchar PRIMARY KEY NOT NULL, + "stat" varchar, + "label" varchar + ); + + CREATE TABLE IF NOT EXISTS "customers_misc" ( + "_order" integer NOT NULL, + "_parent_id" integer NOT NULL, + "id" varchar PRIMARY KEY NOT NULL, + "label" varchar, + "text" varchar + ); + + CREATE TABLE IF NOT EXISTS "customers_industry" ( + "order" integer NOT NULL, + "parent_id" integer NOT NULL, + "value" "enum_customers_industry", + "id" serial PRIMARY KEY NOT NULL + ); + + CREATE TABLE IF NOT EXISTS "customers_supabase_products" ( + "order" integer NOT NULL, + "parent_id" integer NOT NULL, + "value" "enum_customers_supabase_products", + "id" serial PRIMARY KEY NOT NULL + ); + + CREATE TABLE IF NOT EXISTS "customers" ( + "id" serial PRIMARY KEY NOT NULL, + "name" varchar, + "title" varchar, + "slug" varchar, + "slug_lock" boolean DEFAULT true, + "content" jsonb, + "description" varchar, + "about" varchar, + "company_url" varchar, + "company_size" "enum_customers_company_size", + "region" "enum_customers_region", + "meta_title" varchar, + "meta_image_id" integer, + "meta_description" varchar, + "published_at" timestamp(3) with time zone, + "logo_id" integer, + "logo_inverse_id" integer, + "updated_at" timestamp(3) with time zone DEFAULT now() NOT NULL, + "created_at" timestamp(3) with time zone DEFAULT now() NOT NULL, + "_status" "enum_customers_status" DEFAULT 'draft' + ); + + CREATE TABLE IF NOT EXISTS "_customers_v_version_stats" ( + "_order" integer NOT NULL, + "_parent_id" integer NOT NULL, + "id" serial PRIMARY KEY NOT NULL, + "stat" varchar, + "label" varchar, + "_uuid" varchar + ); + + CREATE TABLE IF NOT EXISTS "_customers_v_version_misc" ( + "_order" integer NOT NULL, + "_parent_id" integer NOT NULL, + "id" serial PRIMARY KEY NOT NULL, + "label" varchar, + "text" varchar, + "_uuid" varchar + ); + + CREATE TABLE IF NOT EXISTS "_customers_v_version_industry" ( + "order" integer NOT NULL, + "parent_id" integer NOT NULL, + "value" "enum__customers_v_version_industry", + "id" serial PRIMARY KEY NOT NULL + ); + + CREATE TABLE IF NOT EXISTS "_customers_v_version_supabase_products" ( + "order" integer NOT NULL, + "parent_id" integer NOT NULL, + "value" "enum__customers_v_version_supabase_products", + "id" serial PRIMARY KEY NOT NULL + ); + + CREATE TABLE IF NOT EXISTS "_customers_v" ( + "id" serial PRIMARY KEY NOT NULL, + "parent_id" integer, + "version_name" varchar, + "version_title" varchar, + "version_slug" varchar, + "version_slug_lock" boolean DEFAULT true, + "version_content" jsonb, + "version_description" varchar, + "version_about" varchar, + "version_company_url" varchar, + "version_company_size" "enum__customers_v_version_company_size", + "version_region" "enum__customers_v_version_region", + "version_meta_title" varchar, + "version_meta_image_id" integer, + "version_meta_description" varchar, + "version_published_at" timestamp(3) with time zone, + "version_logo_id" integer, + "version_logo_inverse_id" integer, + "version_updated_at" timestamp(3) with time zone, + "version_created_at" timestamp(3) with time zone, + "version__status" "enum__customers_v_version_status" DEFAULT 'draft', + "created_at" timestamp(3) with time zone DEFAULT now() NOT NULL, + "updated_at" timestamp(3) with time zone DEFAULT now() NOT NULL, + "latest" boolean, + "autosave" boolean + ); + + CREATE TABLE IF NOT EXISTS "events_type" ( + "order" integer NOT NULL, + "parent_id" integer NOT NULL, + "value" "enum_events_type", + "id" serial PRIMARY KEY NOT NULL + ); + + CREATE TABLE IF NOT EXISTS "events" ( + "id" serial PRIMARY KEY NOT NULL, + "title" varchar, + "slug" varchar, + "slug_lock" boolean DEFAULT true, + "subtitle" varchar, + "content" jsonb, + "thumb_id" integer, + "image_id" integer, + "date" timestamp(3) with time zone, + "timezone" "enum_events_timezone", + "show_end_date" boolean DEFAULT false, + "end_date" timestamp(3) with time zone, + "duration" varchar, + "on_demand" boolean DEFAULT false, + "disable_page_build" boolean DEFAULT false, + "link_href" varchar, + "link_target" "enum_events_link_target" DEFAULT '_blank', + "main_cta_href" varchar, + "main_cta_target" "enum_events_main_cta_target" DEFAULT '_blank', + "main_cta_label" varchar, + "main_cta_disabled" boolean DEFAULT false, + "main_cta_disabled_label" varchar, + "company_show_company" boolean DEFAULT false, + "company_name" varchar, + "company_website_url" varchar, + "company_logo_id" integer, + "company_logo_light_id" integer, + "participants_show_participants" boolean DEFAULT false, + "meta_title" varchar, + "meta_image_id" integer, + "meta_description" varchar, + "published_at" timestamp(3) with time zone, + "updated_at" timestamp(3) with time zone DEFAULT now() NOT NULL, + "created_at" timestamp(3) with time zone DEFAULT now() NOT NULL, + "_status" "enum_events_status" DEFAULT 'draft' + ); + + CREATE TABLE IF NOT EXISTS "events_rels" ( + "id" serial PRIMARY KEY NOT NULL, + "order" integer, + "parent_id" integer NOT NULL, + "path" varchar NOT NULL, + "authors_id" integer + ); + + CREATE TABLE IF NOT EXISTS "_events_v_version_type" ( + "order" integer NOT NULL, + "parent_id" integer NOT NULL, + "value" "enum__events_v_version_type", + "id" serial PRIMARY KEY NOT NULL + ); + + CREATE TABLE IF NOT EXISTS "_events_v" ( + "id" serial PRIMARY KEY NOT NULL, + "parent_id" integer, + "version_title" varchar, + "version_slug" varchar, + "version_slug_lock" boolean DEFAULT true, + "version_subtitle" varchar, + "version_content" jsonb, + "version_thumb_id" integer, + "version_image_id" integer, + "version_date" timestamp(3) with time zone, + "version_timezone" "enum__events_v_version_timezone", + "version_show_end_date" boolean DEFAULT false, + "version_end_date" timestamp(3) with time zone, + "version_duration" varchar, + "version_on_demand" boolean DEFAULT false, + "version_disable_page_build" boolean DEFAULT false, + "version_link_href" varchar, + "version_link_target" "enum__events_v_version_link_target" DEFAULT '_blank', + "version_main_cta_href" varchar, + "version_main_cta_target" "enum__events_v_version_main_cta_target" DEFAULT '_blank', + "version_main_cta_label" varchar, + "version_main_cta_disabled" boolean DEFAULT false, + "version_main_cta_disabled_label" varchar, + "version_company_show_company" boolean DEFAULT false, + "version_company_name" varchar, + "version_company_website_url" varchar, + "version_company_logo_id" integer, + "version_company_logo_light_id" integer, + "version_participants_show_participants" boolean DEFAULT false, + "version_meta_title" varchar, + "version_meta_image_id" integer, + "version_meta_description" varchar, + "version_published_at" timestamp(3) with time zone, + "version_updated_at" timestamp(3) with time zone, + "version_created_at" timestamp(3) with time zone, + "version__status" "enum__events_v_version_status" DEFAULT 'draft', + "created_at" timestamp(3) with time zone DEFAULT now() NOT NULL, + "updated_at" timestamp(3) with time zone DEFAULT now() NOT NULL, + "latest" boolean, + "autosave" boolean + ); + + CREATE TABLE IF NOT EXISTS "_events_v_rels" ( + "id" serial PRIMARY KEY NOT NULL, + "order" integer, + "parent_id" integer NOT NULL, + "path" varchar NOT NULL, + "authors_id" integer + ); + + CREATE TABLE IF NOT EXISTS "media" ( + "id" serial PRIMARY KEY NOT NULL, + "alt" varchar, + "caption" jsonb, + "prefix" varchar DEFAULT 'media', + "updated_at" timestamp(3) with time zone DEFAULT now() NOT NULL, + "created_at" timestamp(3) with time zone DEFAULT now() NOT NULL, + "url" varchar, + "thumbnail_u_r_l" varchar, + "filename" varchar, + "mime_type" varchar, + "filesize" numeric, + "width" numeric, + "height" numeric, + "focal_x" numeric, + "focal_y" numeric, + "sizes_thumbnail_url" varchar, + "sizes_thumbnail_width" numeric, + "sizes_thumbnail_height" numeric, + "sizes_thumbnail_mime_type" varchar, + "sizes_thumbnail_filesize" numeric, + "sizes_thumbnail_filename" varchar, + "sizes_square_url" varchar, + "sizes_square_width" numeric, + "sizes_square_height" numeric, + "sizes_square_mime_type" varchar, + "sizes_square_filesize" numeric, + "sizes_square_filename" varchar, + "sizes_small_url" varchar, + "sizes_small_width" numeric, + "sizes_small_height" numeric, + "sizes_small_mime_type" varchar, + "sizes_small_filesize" numeric, + "sizes_small_filename" varchar, + "sizes_medium_url" varchar, + "sizes_medium_width" numeric, + "sizes_medium_height" numeric, + "sizes_medium_mime_type" varchar, + "sizes_medium_filesize" numeric, + "sizes_medium_filename" varchar, + "sizes_large_url" varchar, + "sizes_large_width" numeric, + "sizes_large_height" numeric, + "sizes_large_mime_type" varchar, + "sizes_large_filesize" numeric, + "sizes_large_filename" varchar, + "sizes_xlarge_url" varchar, + "sizes_xlarge_width" numeric, + "sizes_xlarge_height" numeric, + "sizes_xlarge_mime_type" varchar, + "sizes_xlarge_filesize" numeric, + "sizes_xlarge_filename" varchar, + "sizes_og_url" varchar, + "sizes_og_width" numeric, + "sizes_og_height" numeric, + "sizes_og_mime_type" varchar, + "sizes_og_filesize" numeric, + "sizes_og_filename" varchar + ); + + CREATE TABLE IF NOT EXISTS "posts" ( + "id" serial PRIMARY KEY NOT NULL, + "title" varchar, + "slug" varchar, + "slug_lock" boolean DEFAULT true, + "content" jsonb, + "thumb_id" integer, + "image_id" integer, + "launchweek" "enum_posts_launchweek", + "reading_time" numeric, + "date" timestamp(3) with time zone, + "toc_depth" numeric DEFAULT 2, + "description" varchar, + "meta_title" varchar, + "meta_image_id" integer, + "meta_description" varchar, + "published_at" timestamp(3) with time zone, + "updated_at" timestamp(3) with time zone DEFAULT now() NOT NULL, + "created_at" timestamp(3) with time zone DEFAULT now() NOT NULL, + "_status" "enum_posts_status" DEFAULT 'draft' + ); + + CREATE TABLE IF NOT EXISTS "posts_rels" ( + "id" serial PRIMARY KEY NOT NULL, + "order" integer, + "parent_id" integer NOT NULL, + "path" varchar NOT NULL, + "categories_id" integer, + "authors_id" integer, + "tags_id" integer + ); + + CREATE TABLE IF NOT EXISTS "_posts_v" ( + "id" serial PRIMARY KEY NOT NULL, + "parent_id" integer, + "version_title" varchar, + "version_slug" varchar, + "version_slug_lock" boolean DEFAULT true, + "version_content" jsonb, + "version_thumb_id" integer, + "version_image_id" integer, + "version_launchweek" "enum__posts_v_version_launchweek", + "version_reading_time" numeric, + "version_date" timestamp(3) with time zone, + "version_toc_depth" numeric DEFAULT 2, + "version_description" varchar, + "version_meta_title" varchar, + "version_meta_image_id" integer, + "version_meta_description" varchar, + "version_published_at" timestamp(3) with time zone, + "version_updated_at" timestamp(3) with time zone, + "version_created_at" timestamp(3) with time zone, + "version__status" "enum__posts_v_version_status" DEFAULT 'draft', + "created_at" timestamp(3) with time zone DEFAULT now() NOT NULL, + "updated_at" timestamp(3) with time zone DEFAULT now() NOT NULL, + "latest" boolean, + "autosave" boolean + ); + + CREATE TABLE IF NOT EXISTS "_posts_v_rels" ( + "id" serial PRIMARY KEY NOT NULL, + "order" integer, + "parent_id" integer NOT NULL, + "path" varchar NOT NULL, + "categories_id" integer, + "authors_id" integer, + "tags_id" integer + ); + + CREATE TABLE IF NOT EXISTS "tags" ( + "id" serial PRIMARY KEY NOT NULL, + "name" varchar NOT NULL, + "updated_at" timestamp(3) with time zone DEFAULT now() NOT NULL, + "created_at" timestamp(3) with time zone DEFAULT now() NOT NULL + ); + + CREATE TABLE IF NOT EXISTS "users_roles" ( + "order" integer NOT NULL, + "parent_id" integer NOT NULL, + "value" "enum_users_roles", + "id" serial PRIMARY KEY NOT NULL + ); + + CREATE TABLE IF NOT EXISTS "users" ( + "id" serial PRIMARY KEY NOT NULL, + "updated_at" timestamp(3) with time zone DEFAULT now() NOT NULL, + "created_at" timestamp(3) with time zone DEFAULT now() NOT NULL, + "email" varchar NOT NULL, + "reset_password_token" varchar, + "reset_password_expiration" timestamp(3) with time zone, + "salt" varchar, + "hash" varchar, + "login_attempts" numeric DEFAULT 0, + "lock_until" timestamp(3) with time zone + ); + + CREATE TABLE IF NOT EXISTS "payload_jobs_log" ( + "_order" integer NOT NULL, + "_parent_id" integer NOT NULL, + "id" varchar PRIMARY KEY NOT NULL, + "executed_at" timestamp(3) with time zone NOT NULL, + "completed_at" timestamp(3) with time zone NOT NULL, + "task_slug" "enum_payload_jobs_log_task_slug" NOT NULL, + "task_i_d" varchar NOT NULL, + "input" jsonb, + "output" jsonb, + "state" "enum_payload_jobs_log_state" NOT NULL, + "error" jsonb + ); + + CREATE TABLE IF NOT EXISTS "payload_jobs" ( + "id" serial PRIMARY KEY NOT NULL, + "input" jsonb, + "completed_at" timestamp(3) with time zone, + "total_tried" numeric DEFAULT 0, + "has_error" boolean DEFAULT false, + "error" jsonb, + "task_slug" "enum_payload_jobs_task_slug", + "queue" varchar DEFAULT 'default', + "wait_until" timestamp(3) with time zone, + "processing" boolean DEFAULT false, + "updated_at" timestamp(3) with time zone DEFAULT now() NOT NULL, + "created_at" timestamp(3) with time zone DEFAULT now() NOT NULL + ); + + CREATE TABLE IF NOT EXISTS "payload_locked_documents" ( + "id" serial PRIMARY KEY NOT NULL, + "global_slug" varchar, + "updated_at" timestamp(3) with time zone DEFAULT now() NOT NULL, + "created_at" timestamp(3) with time zone DEFAULT now() NOT NULL + ); + + CREATE TABLE IF NOT EXISTS "payload_locked_documents_rels" ( + "id" serial PRIMARY KEY NOT NULL, + "order" integer, + "parent_id" integer NOT NULL, + "path" varchar NOT NULL, + "authors_id" integer, + "categories_id" integer, + "customers_id" integer, + "events_id" integer, + "media_id" integer, + "posts_id" integer, + "tags_id" integer, + "users_id" integer, + "payload_jobs_id" integer + ); + + CREATE TABLE IF NOT EXISTS "payload_preferences" ( + "id" serial PRIMARY KEY NOT NULL, + "key" varchar, + "value" jsonb, + "updated_at" timestamp(3) with time zone DEFAULT now() NOT NULL, + "created_at" timestamp(3) with time zone DEFAULT now() NOT NULL + ); + + CREATE TABLE IF NOT EXISTS "payload_preferences_rels" ( + "id" serial PRIMARY KEY NOT NULL, + "order" integer, + "parent_id" integer NOT NULL, + "path" varchar NOT NULL, + "users_id" integer + ); + + CREATE TABLE IF NOT EXISTS "payload_migrations" ( + "id" serial PRIMARY KEY NOT NULL, + "name" varchar, + "batch" numeric, + "updated_at" timestamp(3) with time zone DEFAULT now() NOT NULL, + "created_at" timestamp(3) with time zone DEFAULT now() NOT NULL + ); + + DO $$ BEGIN + ALTER TABLE "authors" ADD CONSTRAINT "authors_author_image_url_id_media_id_fk" FOREIGN KEY ("author_image_url_id") REFERENCES "public"."media"("id") ON DELETE set null ON UPDATE no action; + EXCEPTION + WHEN duplicate_object THEN null; + END $$; + + DO $$ BEGIN + ALTER TABLE "categories_breadcrumbs" ADD CONSTRAINT "categories_breadcrumbs_doc_id_categories_id_fk" FOREIGN KEY ("doc_id") REFERENCES "public"."categories"("id") ON DELETE set null ON UPDATE no action; + EXCEPTION + WHEN duplicate_object THEN null; + END $$; + + DO $$ BEGIN + ALTER TABLE "categories_breadcrumbs" ADD CONSTRAINT "categories_breadcrumbs_parent_id_fk" FOREIGN KEY ("_parent_id") REFERENCES "public"."categories"("id") ON DELETE cascade ON UPDATE no action; + EXCEPTION + WHEN duplicate_object THEN null; + END $$; + + DO $$ BEGIN + ALTER TABLE "categories" ADD CONSTRAINT "categories_parent_id_categories_id_fk" FOREIGN KEY ("parent_id") REFERENCES "public"."categories"("id") ON DELETE set null ON UPDATE no action; + EXCEPTION + WHEN duplicate_object THEN null; + END $$; + + DO $$ BEGIN + ALTER TABLE "customers_stats" ADD CONSTRAINT "customers_stats_parent_id_fk" FOREIGN KEY ("_parent_id") REFERENCES "public"."customers"("id") ON DELETE cascade ON UPDATE no action; + EXCEPTION + WHEN duplicate_object THEN null; + END $$; + + DO $$ BEGIN + ALTER TABLE "customers_misc" ADD CONSTRAINT "customers_misc_parent_id_fk" FOREIGN KEY ("_parent_id") REFERENCES "public"."customers"("id") ON DELETE cascade ON UPDATE no action; + EXCEPTION + WHEN duplicate_object THEN null; + END $$; + + DO $$ BEGIN + ALTER TABLE "customers_industry" ADD CONSTRAINT "customers_industry_parent_fk" FOREIGN KEY ("parent_id") REFERENCES "public"."customers"("id") ON DELETE cascade ON UPDATE no action; + EXCEPTION + WHEN duplicate_object THEN null; + END $$; + + DO $$ BEGIN + ALTER TABLE "customers_supabase_products" ADD CONSTRAINT "customers_supabase_products_parent_fk" FOREIGN KEY ("parent_id") REFERENCES "public"."customers"("id") ON DELETE cascade ON UPDATE no action; + EXCEPTION + WHEN duplicate_object THEN null; + END $$; + + DO $$ BEGIN + ALTER TABLE "customers" ADD CONSTRAINT "customers_meta_image_id_media_id_fk" FOREIGN KEY ("meta_image_id") REFERENCES "public"."media"("id") ON DELETE set null ON UPDATE no action; + EXCEPTION + WHEN duplicate_object THEN null; + END $$; + + DO $$ BEGIN + ALTER TABLE "customers" ADD CONSTRAINT "customers_logo_id_media_id_fk" FOREIGN KEY ("logo_id") REFERENCES "public"."media"("id") ON DELETE set null ON UPDATE no action; + EXCEPTION + WHEN duplicate_object THEN null; + END $$; + + DO $$ BEGIN + ALTER TABLE "customers" ADD CONSTRAINT "customers_logo_inverse_id_media_id_fk" FOREIGN KEY ("logo_inverse_id") REFERENCES "public"."media"("id") ON DELETE set null ON UPDATE no action; + EXCEPTION + WHEN duplicate_object THEN null; + END $$; + + DO $$ BEGIN + ALTER TABLE "_customers_v_version_stats" ADD CONSTRAINT "_customers_v_version_stats_parent_id_fk" FOREIGN KEY ("_parent_id") REFERENCES "public"."_customers_v"("id") ON DELETE cascade ON UPDATE no action; + EXCEPTION + WHEN duplicate_object THEN null; + END $$; + + DO $$ BEGIN + ALTER TABLE "_customers_v_version_misc" ADD CONSTRAINT "_customers_v_version_misc_parent_id_fk" FOREIGN KEY ("_parent_id") REFERENCES "public"."_customers_v"("id") ON DELETE cascade ON UPDATE no action; + EXCEPTION + WHEN duplicate_object THEN null; + END $$; + + DO $$ BEGIN + ALTER TABLE "_customers_v_version_industry" ADD CONSTRAINT "_customers_v_version_industry_parent_fk" FOREIGN KEY ("parent_id") REFERENCES "public"."_customers_v"("id") ON DELETE cascade ON UPDATE no action; + EXCEPTION + WHEN duplicate_object THEN null; + END $$; + + DO $$ BEGIN + ALTER TABLE "_customers_v_version_supabase_products" ADD CONSTRAINT "_customers_v_version_supabase_products_parent_fk" FOREIGN KEY ("parent_id") REFERENCES "public"."_customers_v"("id") ON DELETE cascade ON UPDATE no action; + EXCEPTION + WHEN duplicate_object THEN null; + END $$; + + DO $$ BEGIN + ALTER TABLE "_customers_v" ADD CONSTRAINT "_customers_v_parent_id_customers_id_fk" FOREIGN KEY ("parent_id") REFERENCES "public"."customers"("id") ON DELETE set null ON UPDATE no action; + EXCEPTION + WHEN duplicate_object THEN null; + END $$; + + DO $$ BEGIN + ALTER TABLE "_customers_v" ADD CONSTRAINT "_customers_v_version_meta_image_id_media_id_fk" FOREIGN KEY ("version_meta_image_id") REFERENCES "public"."media"("id") ON DELETE set null ON UPDATE no action; + EXCEPTION + WHEN duplicate_object THEN null; + END $$; + + DO $$ BEGIN + ALTER TABLE "_customers_v" ADD CONSTRAINT "_customers_v_version_logo_id_media_id_fk" FOREIGN KEY ("version_logo_id") REFERENCES "public"."media"("id") ON DELETE set null ON UPDATE no action; + EXCEPTION + WHEN duplicate_object THEN null; + END $$; + + DO $$ BEGIN + ALTER TABLE "_customers_v" ADD CONSTRAINT "_customers_v_version_logo_inverse_id_media_id_fk" FOREIGN KEY ("version_logo_inverse_id") REFERENCES "public"."media"("id") ON DELETE set null ON UPDATE no action; + EXCEPTION + WHEN duplicate_object THEN null; + END $$; + + DO $$ BEGIN + ALTER TABLE "events_type" ADD CONSTRAINT "events_type_parent_fk" FOREIGN KEY ("parent_id") REFERENCES "public"."events"("id") ON DELETE cascade ON UPDATE no action; + EXCEPTION + WHEN duplicate_object THEN null; + END $$; + + DO $$ BEGIN + ALTER TABLE "events" ADD CONSTRAINT "events_thumb_id_media_id_fk" FOREIGN KEY ("thumb_id") REFERENCES "public"."media"("id") ON DELETE set null ON UPDATE no action; + EXCEPTION + WHEN duplicate_object THEN null; + END $$; + + DO $$ BEGIN + ALTER TABLE "events" ADD CONSTRAINT "events_image_id_media_id_fk" FOREIGN KEY ("image_id") REFERENCES "public"."media"("id") ON DELETE set null ON UPDATE no action; + EXCEPTION + WHEN duplicate_object THEN null; + END $$; + + DO $$ BEGIN + ALTER TABLE "events" ADD CONSTRAINT "events_company_logo_id_media_id_fk" FOREIGN KEY ("company_logo_id") REFERENCES "public"."media"("id") ON DELETE set null ON UPDATE no action; + EXCEPTION + WHEN duplicate_object THEN null; + END $$; + + DO $$ BEGIN + ALTER TABLE "events" ADD CONSTRAINT "events_company_logo_light_id_media_id_fk" FOREIGN KEY ("company_logo_light_id") REFERENCES "public"."media"("id") ON DELETE set null ON UPDATE no action; + EXCEPTION + WHEN duplicate_object THEN null; + END $$; + + DO $$ BEGIN + ALTER TABLE "events" ADD CONSTRAINT "events_meta_image_id_media_id_fk" FOREIGN KEY ("meta_image_id") REFERENCES "public"."media"("id") ON DELETE set null ON UPDATE no action; + EXCEPTION + WHEN duplicate_object THEN null; + END $$; + + DO $$ BEGIN + ALTER TABLE "events_rels" ADD CONSTRAINT "events_rels_parent_fk" FOREIGN KEY ("parent_id") REFERENCES "public"."events"("id") ON DELETE cascade ON UPDATE no action; + EXCEPTION + WHEN duplicate_object THEN null; + END $$; + + DO $$ BEGIN + ALTER TABLE "events_rels" ADD CONSTRAINT "events_rels_authors_fk" FOREIGN KEY ("authors_id") REFERENCES "public"."authors"("id") ON DELETE cascade ON UPDATE no action; + EXCEPTION + WHEN duplicate_object THEN null; + END $$; + + DO $$ BEGIN + ALTER TABLE "_events_v_version_type" ADD CONSTRAINT "_events_v_version_type_parent_fk" FOREIGN KEY ("parent_id") REFERENCES "public"."_events_v"("id") ON DELETE cascade ON UPDATE no action; + EXCEPTION + WHEN duplicate_object THEN null; + END $$; + + DO $$ BEGIN + ALTER TABLE "_events_v" ADD CONSTRAINT "_events_v_parent_id_events_id_fk" FOREIGN KEY ("parent_id") REFERENCES "public"."events"("id") ON DELETE set null ON UPDATE no action; + EXCEPTION + WHEN duplicate_object THEN null; + END $$; + + DO $$ BEGIN + ALTER TABLE "_events_v" ADD CONSTRAINT "_events_v_version_thumb_id_media_id_fk" FOREIGN KEY ("version_thumb_id") REFERENCES "public"."media"("id") ON DELETE set null ON UPDATE no action; + EXCEPTION + WHEN duplicate_object THEN null; + END $$; + + DO $$ BEGIN + ALTER TABLE "_events_v" ADD CONSTRAINT "_events_v_version_image_id_media_id_fk" FOREIGN KEY ("version_image_id") REFERENCES "public"."media"("id") ON DELETE set null ON UPDATE no action; + EXCEPTION + WHEN duplicate_object THEN null; + END $$; + + DO $$ BEGIN + ALTER TABLE "_events_v" ADD CONSTRAINT "_events_v_version_company_logo_id_media_id_fk" FOREIGN KEY ("version_company_logo_id") REFERENCES "public"."media"("id") ON DELETE set null ON UPDATE no action; + EXCEPTION + WHEN duplicate_object THEN null; + END $$; + + DO $$ BEGIN + ALTER TABLE "_events_v" ADD CONSTRAINT "_events_v_version_company_logo_light_id_media_id_fk" FOREIGN KEY ("version_company_logo_light_id") REFERENCES "public"."media"("id") ON DELETE set null ON UPDATE no action; + EXCEPTION + WHEN duplicate_object THEN null; + END $$; + + DO $$ BEGIN + ALTER TABLE "_events_v" ADD CONSTRAINT "_events_v_version_meta_image_id_media_id_fk" FOREIGN KEY ("version_meta_image_id") REFERENCES "public"."media"("id") ON DELETE set null ON UPDATE no action; + EXCEPTION + WHEN duplicate_object THEN null; + END $$; + + DO $$ BEGIN + ALTER TABLE "_events_v_rels" ADD CONSTRAINT "_events_v_rels_parent_fk" FOREIGN KEY ("parent_id") REFERENCES "public"."_events_v"("id") ON DELETE cascade ON UPDATE no action; + EXCEPTION + WHEN duplicate_object THEN null; + END $$; + + DO $$ BEGIN + ALTER TABLE "_events_v_rels" ADD CONSTRAINT "_events_v_rels_authors_fk" FOREIGN KEY ("authors_id") REFERENCES "public"."authors"("id") ON DELETE cascade ON UPDATE no action; + EXCEPTION + WHEN duplicate_object THEN null; + END $$; + + DO $$ BEGIN + ALTER TABLE "posts" ADD CONSTRAINT "posts_thumb_id_media_id_fk" FOREIGN KEY ("thumb_id") REFERENCES "public"."media"("id") ON DELETE set null ON UPDATE no action; + EXCEPTION + WHEN duplicate_object THEN null; + END $$; + + DO $$ BEGIN + ALTER TABLE "posts" ADD CONSTRAINT "posts_image_id_media_id_fk" FOREIGN KEY ("image_id") REFERENCES "public"."media"("id") ON DELETE set null ON UPDATE no action; + EXCEPTION + WHEN duplicate_object THEN null; + END $$; + + DO $$ BEGIN + ALTER TABLE "posts" ADD CONSTRAINT "posts_meta_image_id_media_id_fk" FOREIGN KEY ("meta_image_id") REFERENCES "public"."media"("id") ON DELETE set null ON UPDATE no action; + EXCEPTION + WHEN duplicate_object THEN null; + END $$; + + DO $$ BEGIN + ALTER TABLE "posts_rels" ADD CONSTRAINT "posts_rels_parent_fk" FOREIGN KEY ("parent_id") REFERENCES "public"."posts"("id") ON DELETE cascade ON UPDATE no action; + EXCEPTION + WHEN duplicate_object THEN null; + END $$; + + DO $$ BEGIN + ALTER TABLE "posts_rels" ADD CONSTRAINT "posts_rels_categories_fk" FOREIGN KEY ("categories_id") REFERENCES "public"."categories"("id") ON DELETE cascade ON UPDATE no action; + EXCEPTION + WHEN duplicate_object THEN null; + END $$; + + DO $$ BEGIN + ALTER TABLE "posts_rels" ADD CONSTRAINT "posts_rels_authors_fk" FOREIGN KEY ("authors_id") REFERENCES "public"."authors"("id") ON DELETE cascade ON UPDATE no action; + EXCEPTION + WHEN duplicate_object THEN null; + END $$; + + DO $$ BEGIN + ALTER TABLE "posts_rels" ADD CONSTRAINT "posts_rels_tags_fk" FOREIGN KEY ("tags_id") REFERENCES "public"."tags"("id") ON DELETE cascade ON UPDATE no action; + EXCEPTION + WHEN duplicate_object THEN null; + END $$; + + DO $$ BEGIN + ALTER TABLE "_posts_v" ADD CONSTRAINT "_posts_v_parent_id_posts_id_fk" FOREIGN KEY ("parent_id") REFERENCES "public"."posts"("id") ON DELETE set null ON UPDATE no action; + EXCEPTION + WHEN duplicate_object THEN null; + END $$; + + DO $$ BEGIN + ALTER TABLE "_posts_v" ADD CONSTRAINT "_posts_v_version_thumb_id_media_id_fk" FOREIGN KEY ("version_thumb_id") REFERENCES "public"."media"("id") ON DELETE set null ON UPDATE no action; + EXCEPTION + WHEN duplicate_object THEN null; + END $$; + + DO $$ BEGIN + ALTER TABLE "_posts_v" ADD CONSTRAINT "_posts_v_version_image_id_media_id_fk" FOREIGN KEY ("version_image_id") REFERENCES "public"."media"("id") ON DELETE set null ON UPDATE no action; + EXCEPTION + WHEN duplicate_object THEN null; + END $$; + + DO $$ BEGIN + ALTER TABLE "_posts_v" ADD CONSTRAINT "_posts_v_version_meta_image_id_media_id_fk" FOREIGN KEY ("version_meta_image_id") REFERENCES "public"."media"("id") ON DELETE set null ON UPDATE no action; + EXCEPTION + WHEN duplicate_object THEN null; + END $$; + + DO $$ BEGIN + ALTER TABLE "_posts_v_rels" ADD CONSTRAINT "_posts_v_rels_parent_fk" FOREIGN KEY ("parent_id") REFERENCES "public"."_posts_v"("id") ON DELETE cascade ON UPDATE no action; + EXCEPTION + WHEN duplicate_object THEN null; + END $$; + + DO $$ BEGIN + ALTER TABLE "_posts_v_rels" ADD CONSTRAINT "_posts_v_rels_categories_fk" FOREIGN KEY ("categories_id") REFERENCES "public"."categories"("id") ON DELETE cascade ON UPDATE no action; + EXCEPTION + WHEN duplicate_object THEN null; + END $$; + + DO $$ BEGIN + ALTER TABLE "_posts_v_rels" ADD CONSTRAINT "_posts_v_rels_authors_fk" FOREIGN KEY ("authors_id") REFERENCES "public"."authors"("id") ON DELETE cascade ON UPDATE no action; + EXCEPTION + WHEN duplicate_object THEN null; + END $$; + + DO $$ BEGIN + ALTER TABLE "_posts_v_rels" ADD CONSTRAINT "_posts_v_rels_tags_fk" FOREIGN KEY ("tags_id") REFERENCES "public"."tags"("id") ON DELETE cascade ON UPDATE no action; + EXCEPTION + WHEN duplicate_object THEN null; + END $$; + + DO $$ BEGIN + ALTER TABLE "users_roles" ADD CONSTRAINT "users_roles_parent_fk" FOREIGN KEY ("parent_id") REFERENCES "public"."users"("id") ON DELETE cascade ON UPDATE no action; + EXCEPTION + WHEN duplicate_object THEN null; + END $$; + + DO $$ BEGIN + ALTER TABLE "payload_jobs_log" ADD CONSTRAINT "payload_jobs_log_parent_id_fk" FOREIGN KEY ("_parent_id") REFERENCES "public"."payload_jobs"("id") ON DELETE cascade ON UPDATE no action; + EXCEPTION + WHEN duplicate_object THEN null; + END $$; + + DO $$ BEGIN + ALTER TABLE "payload_locked_documents_rels" ADD CONSTRAINT "payload_locked_documents_rels_parent_fk" FOREIGN KEY ("parent_id") REFERENCES "public"."payload_locked_documents"("id") ON DELETE cascade ON UPDATE no action; + EXCEPTION + WHEN duplicate_object THEN null; + END $$; + + DO $$ BEGIN + ALTER TABLE "payload_locked_documents_rels" ADD CONSTRAINT "payload_locked_documents_rels_authors_fk" FOREIGN KEY ("authors_id") REFERENCES "public"."authors"("id") ON DELETE cascade ON UPDATE no action; + EXCEPTION + WHEN duplicate_object THEN null; + END $$; + + DO $$ BEGIN + ALTER TABLE "payload_locked_documents_rels" ADD CONSTRAINT "payload_locked_documents_rels_categories_fk" FOREIGN KEY ("categories_id") REFERENCES "public"."categories"("id") ON DELETE cascade ON UPDATE no action; + EXCEPTION + WHEN duplicate_object THEN null; + END $$; + + DO $$ BEGIN + ALTER TABLE "payload_locked_documents_rels" ADD CONSTRAINT "payload_locked_documents_rels_customers_fk" FOREIGN KEY ("customers_id") REFERENCES "public"."customers"("id") ON DELETE cascade ON UPDATE no action; + EXCEPTION + WHEN duplicate_object THEN null; + END $$; + + DO $$ BEGIN + ALTER TABLE "payload_locked_documents_rels" ADD CONSTRAINT "payload_locked_documents_rels_events_fk" FOREIGN KEY ("events_id") REFERENCES "public"."events"("id") ON DELETE cascade ON UPDATE no action; + EXCEPTION + WHEN duplicate_object THEN null; + END $$; + + DO $$ BEGIN + ALTER TABLE "payload_locked_documents_rels" ADD CONSTRAINT "payload_locked_documents_rels_media_fk" FOREIGN KEY ("media_id") REFERENCES "public"."media"("id") ON DELETE cascade ON UPDATE no action; + EXCEPTION + WHEN duplicate_object THEN null; + END $$; + + DO $$ BEGIN + ALTER TABLE "payload_locked_documents_rels" ADD CONSTRAINT "payload_locked_documents_rels_posts_fk" FOREIGN KEY ("posts_id") REFERENCES "public"."posts"("id") ON DELETE cascade ON UPDATE no action; + EXCEPTION + WHEN duplicate_object THEN null; + END $$; + + DO $$ BEGIN + ALTER TABLE "payload_locked_documents_rels" ADD CONSTRAINT "payload_locked_documents_rels_tags_fk" FOREIGN KEY ("tags_id") REFERENCES "public"."tags"("id") ON DELETE cascade ON UPDATE no action; + EXCEPTION + WHEN duplicate_object THEN null; + END $$; + + DO $$ BEGIN + ALTER TABLE "payload_locked_documents_rels" ADD CONSTRAINT "payload_locked_documents_rels_users_fk" FOREIGN KEY ("users_id") REFERENCES "public"."users"("id") ON DELETE cascade ON UPDATE no action; + EXCEPTION + WHEN duplicate_object THEN null; + END $$; + + DO $$ BEGIN + ALTER TABLE "payload_locked_documents_rels" ADD CONSTRAINT "payload_locked_documents_rels_payload_jobs_fk" FOREIGN KEY ("payload_jobs_id") REFERENCES "public"."payload_jobs"("id") ON DELETE cascade ON UPDATE no action; + EXCEPTION + WHEN duplicate_object THEN null; + END $$; + + DO $$ BEGIN + ALTER TABLE "payload_preferences_rels" ADD CONSTRAINT "payload_preferences_rels_parent_fk" FOREIGN KEY ("parent_id") REFERENCES "public"."payload_preferences"("id") ON DELETE cascade ON UPDATE no action; + EXCEPTION + WHEN duplicate_object THEN null; + END $$; + + DO $$ BEGIN + ALTER TABLE "payload_preferences_rels" ADD CONSTRAINT "payload_preferences_rels_users_fk" FOREIGN KEY ("users_id") REFERENCES "public"."users"("id") ON DELETE cascade ON UPDATE no action; + EXCEPTION + WHEN duplicate_object THEN null; + END $$; + + CREATE INDEX IF NOT EXISTS "authors_author_image_url_idx" ON "authors" USING btree ("author_image_url_id"); + CREATE INDEX IF NOT EXISTS "authors_updated_at_idx" ON "authors" USING btree ("updated_at"); + CREATE INDEX IF NOT EXISTS "authors_created_at_idx" ON "authors" USING btree ("created_at"); + CREATE INDEX IF NOT EXISTS "categories_breadcrumbs_order_idx" ON "categories_breadcrumbs" USING btree ("_order"); + CREATE INDEX IF NOT EXISTS "categories_breadcrumbs_parent_id_idx" ON "categories_breadcrumbs" USING btree ("_parent_id"); + CREATE INDEX IF NOT EXISTS "categories_breadcrumbs_doc_idx" ON "categories_breadcrumbs" USING btree ("doc_id"); + CREATE INDEX IF NOT EXISTS "categories_parent_idx" ON "categories" USING btree ("parent_id"); + CREATE INDEX IF NOT EXISTS "categories_updated_at_idx" ON "categories" USING btree ("updated_at"); + CREATE INDEX IF NOT EXISTS "categories_created_at_idx" ON "categories" USING btree ("created_at"); + CREATE INDEX IF NOT EXISTS "customers_stats_order_idx" ON "customers_stats" USING btree ("_order"); + CREATE INDEX IF NOT EXISTS "customers_stats_parent_id_idx" ON "customers_stats" USING btree ("_parent_id"); + CREATE INDEX IF NOT EXISTS "customers_misc_order_idx" ON "customers_misc" USING btree ("_order"); + CREATE INDEX IF NOT EXISTS "customers_misc_parent_id_idx" ON "customers_misc" USING btree ("_parent_id"); + CREATE INDEX IF NOT EXISTS "customers_industry_order_idx" ON "customers_industry" USING btree ("order"); + CREATE INDEX IF NOT EXISTS "customers_industry_parent_idx" ON "customers_industry" USING btree ("parent_id"); + CREATE INDEX IF NOT EXISTS "customers_supabase_products_order_idx" ON "customers_supabase_products" USING btree ("order"); + CREATE INDEX IF NOT EXISTS "customers_supabase_products_parent_idx" ON "customers_supabase_products" USING btree ("parent_id"); + CREATE INDEX IF NOT EXISTS "customers_slug_idx" ON "customers" USING btree ("slug"); + CREATE INDEX IF NOT EXISTS "customers_meta_meta_image_idx" ON "customers" USING btree ("meta_image_id"); + CREATE INDEX IF NOT EXISTS "customers_logo_idx" ON "customers" USING btree ("logo_id"); + CREATE INDEX IF NOT EXISTS "customers_logo_inverse_idx" ON "customers" USING btree ("logo_inverse_id"); + CREATE INDEX IF NOT EXISTS "customers_updated_at_idx" ON "customers" USING btree ("updated_at"); + CREATE INDEX IF NOT EXISTS "customers_created_at_idx" ON "customers" USING btree ("created_at"); + CREATE INDEX IF NOT EXISTS "customers__status_idx" ON "customers" USING btree ("_status"); + CREATE INDEX IF NOT EXISTS "_customers_v_version_stats_order_idx" ON "_customers_v_version_stats" USING btree ("_order"); + CREATE INDEX IF NOT EXISTS "_customers_v_version_stats_parent_id_idx" ON "_customers_v_version_stats" USING btree ("_parent_id"); + CREATE INDEX IF NOT EXISTS "_customers_v_version_misc_order_idx" ON "_customers_v_version_misc" USING btree ("_order"); + CREATE INDEX IF NOT EXISTS "_customers_v_version_misc_parent_id_idx" ON "_customers_v_version_misc" USING btree ("_parent_id"); + CREATE INDEX IF NOT EXISTS "_customers_v_version_industry_order_idx" ON "_customers_v_version_industry" USING btree ("order"); + CREATE INDEX IF NOT EXISTS "_customers_v_version_industry_parent_idx" ON "_customers_v_version_industry" USING btree ("parent_id"); + CREATE INDEX IF NOT EXISTS "_customers_v_version_supabase_products_order_idx" ON "_customers_v_version_supabase_products" USING btree ("order"); + CREATE INDEX IF NOT EXISTS "_customers_v_version_supabase_products_parent_idx" ON "_customers_v_version_supabase_products" USING btree ("parent_id"); + CREATE INDEX IF NOT EXISTS "_customers_v_parent_idx" ON "_customers_v" USING btree ("parent_id"); + CREATE INDEX IF NOT EXISTS "_customers_v_version_version_slug_idx" ON "_customers_v" USING btree ("version_slug"); + CREATE INDEX IF NOT EXISTS "_customers_v_version_meta_version_meta_image_idx" ON "_customers_v" USING btree ("version_meta_image_id"); + CREATE INDEX IF NOT EXISTS "_customers_v_version_version_logo_idx" ON "_customers_v" USING btree ("version_logo_id"); + CREATE INDEX IF NOT EXISTS "_customers_v_version_version_logo_inverse_idx" ON "_customers_v" USING btree ("version_logo_inverse_id"); + CREATE INDEX IF NOT EXISTS "_customers_v_version_version_updated_at_idx" ON "_customers_v" USING btree ("version_updated_at"); + CREATE INDEX IF NOT EXISTS "_customers_v_version_version_created_at_idx" ON "_customers_v" USING btree ("version_created_at"); + CREATE INDEX IF NOT EXISTS "_customers_v_version_version__status_idx" ON "_customers_v" USING btree ("version__status"); + CREATE INDEX IF NOT EXISTS "_customers_v_created_at_idx" ON "_customers_v" USING btree ("created_at"); + CREATE INDEX IF NOT EXISTS "_customers_v_updated_at_idx" ON "_customers_v" USING btree ("updated_at"); + CREATE INDEX IF NOT EXISTS "_customers_v_latest_idx" ON "_customers_v" USING btree ("latest"); + CREATE INDEX IF NOT EXISTS "_customers_v_autosave_idx" ON "_customers_v" USING btree ("autosave"); + CREATE INDEX IF NOT EXISTS "events_type_order_idx" ON "events_type" USING btree ("order"); + CREATE INDEX IF NOT EXISTS "events_type_parent_idx" ON "events_type" USING btree ("parent_id"); + CREATE INDEX IF NOT EXISTS "events_slug_idx" ON "events" USING btree ("slug"); + CREATE INDEX IF NOT EXISTS "events_thumb_idx" ON "events" USING btree ("thumb_id"); + CREATE INDEX IF NOT EXISTS "events_image_idx" ON "events" USING btree ("image_id"); + CREATE INDEX IF NOT EXISTS "events_company_company_logo_idx" ON "events" USING btree ("company_logo_id"); + CREATE INDEX IF NOT EXISTS "events_company_company_logo_light_idx" ON "events" USING btree ("company_logo_light_id"); + CREATE INDEX IF NOT EXISTS "events_meta_meta_image_idx" ON "events" USING btree ("meta_image_id"); + CREATE INDEX IF NOT EXISTS "events_updated_at_idx" ON "events" USING btree ("updated_at"); + CREATE INDEX IF NOT EXISTS "events_created_at_idx" ON "events" USING btree ("created_at"); + CREATE INDEX IF NOT EXISTS "events__status_idx" ON "events" USING btree ("_status"); + CREATE INDEX IF NOT EXISTS "events_rels_order_idx" ON "events_rels" USING btree ("order"); + CREATE INDEX IF NOT EXISTS "events_rels_parent_idx" ON "events_rels" USING btree ("parent_id"); + CREATE INDEX IF NOT EXISTS "events_rels_path_idx" ON "events_rels" USING btree ("path"); + CREATE INDEX IF NOT EXISTS "events_rels_authors_id_idx" ON "events_rels" USING btree ("authors_id"); + CREATE INDEX IF NOT EXISTS "_events_v_version_type_order_idx" ON "_events_v_version_type" USING btree ("order"); + CREATE INDEX IF NOT EXISTS "_events_v_version_type_parent_idx" ON "_events_v_version_type" USING btree ("parent_id"); + CREATE INDEX IF NOT EXISTS "_events_v_parent_idx" ON "_events_v" USING btree ("parent_id"); + CREATE INDEX IF NOT EXISTS "_events_v_version_version_slug_idx" ON "_events_v" USING btree ("version_slug"); + CREATE INDEX IF NOT EXISTS "_events_v_version_version_thumb_idx" ON "_events_v" USING btree ("version_thumb_id"); + CREATE INDEX IF NOT EXISTS "_events_v_version_version_image_idx" ON "_events_v" USING btree ("version_image_id"); + CREATE INDEX IF NOT EXISTS "_events_v_version_company_version_company_logo_idx" ON "_events_v" USING btree ("version_company_logo_id"); + CREATE INDEX IF NOT EXISTS "_events_v_version_company_version_company_logo_light_idx" ON "_events_v" USING btree ("version_company_logo_light_id"); + CREATE INDEX IF NOT EXISTS "_events_v_version_meta_version_meta_image_idx" ON "_events_v" USING btree ("version_meta_image_id"); + CREATE INDEX IF NOT EXISTS "_events_v_version_version_updated_at_idx" ON "_events_v" USING btree ("version_updated_at"); + CREATE INDEX IF NOT EXISTS "_events_v_version_version_created_at_idx" ON "_events_v" USING btree ("version_created_at"); + CREATE INDEX IF NOT EXISTS "_events_v_version_version__status_idx" ON "_events_v" USING btree ("version__status"); + CREATE INDEX IF NOT EXISTS "_events_v_created_at_idx" ON "_events_v" USING btree ("created_at"); + CREATE INDEX IF NOT EXISTS "_events_v_updated_at_idx" ON "_events_v" USING btree ("updated_at"); + CREATE INDEX IF NOT EXISTS "_events_v_latest_idx" ON "_events_v" USING btree ("latest"); + CREATE INDEX IF NOT EXISTS "_events_v_autosave_idx" ON "_events_v" USING btree ("autosave"); + CREATE INDEX IF NOT EXISTS "_events_v_rels_order_idx" ON "_events_v_rels" USING btree ("order"); + CREATE INDEX IF NOT EXISTS "_events_v_rels_parent_idx" ON "_events_v_rels" USING btree ("parent_id"); + CREATE INDEX IF NOT EXISTS "_events_v_rels_path_idx" ON "_events_v_rels" USING btree ("path"); + CREATE INDEX IF NOT EXISTS "_events_v_rels_authors_id_idx" ON "_events_v_rels" USING btree ("authors_id"); + CREATE INDEX IF NOT EXISTS "media_updated_at_idx" ON "media" USING btree ("updated_at"); + CREATE INDEX IF NOT EXISTS "media_created_at_idx" ON "media" USING btree ("created_at"); + CREATE UNIQUE INDEX IF NOT EXISTS "media_filename_idx" ON "media" USING btree ("filename"); + CREATE INDEX IF NOT EXISTS "media_sizes_thumbnail_sizes_thumbnail_filename_idx" ON "media" USING btree ("sizes_thumbnail_filename"); + CREATE INDEX IF NOT EXISTS "media_sizes_square_sizes_square_filename_idx" ON "media" USING btree ("sizes_square_filename"); + CREATE INDEX IF NOT EXISTS "media_sizes_small_sizes_small_filename_idx" ON "media" USING btree ("sizes_small_filename"); + CREATE INDEX IF NOT EXISTS "media_sizes_medium_sizes_medium_filename_idx" ON "media" USING btree ("sizes_medium_filename"); + CREATE INDEX IF NOT EXISTS "media_sizes_large_sizes_large_filename_idx" ON "media" USING btree ("sizes_large_filename"); + CREATE INDEX IF NOT EXISTS "media_sizes_xlarge_sizes_xlarge_filename_idx" ON "media" USING btree ("sizes_xlarge_filename"); + CREATE INDEX IF NOT EXISTS "media_sizes_og_sizes_og_filename_idx" ON "media" USING btree ("sizes_og_filename"); + CREATE INDEX IF NOT EXISTS "posts_slug_idx" ON "posts" USING btree ("slug"); + CREATE INDEX IF NOT EXISTS "posts_thumb_idx" ON "posts" USING btree ("thumb_id"); + CREATE INDEX IF NOT EXISTS "posts_image_idx" ON "posts" USING btree ("image_id"); + CREATE INDEX IF NOT EXISTS "posts_meta_meta_image_idx" ON "posts" USING btree ("meta_image_id"); + CREATE INDEX IF NOT EXISTS "posts_updated_at_idx" ON "posts" USING btree ("updated_at"); + CREATE INDEX IF NOT EXISTS "posts_created_at_idx" ON "posts" USING btree ("created_at"); + CREATE INDEX IF NOT EXISTS "posts__status_idx" ON "posts" USING btree ("_status"); + CREATE INDEX IF NOT EXISTS "posts_rels_order_idx" ON "posts_rels" USING btree ("order"); + CREATE INDEX IF NOT EXISTS "posts_rels_parent_idx" ON "posts_rels" USING btree ("parent_id"); + CREATE INDEX IF NOT EXISTS "posts_rels_path_idx" ON "posts_rels" USING btree ("path"); + CREATE INDEX IF NOT EXISTS "posts_rels_categories_id_idx" ON "posts_rels" USING btree ("categories_id"); + CREATE INDEX IF NOT EXISTS "posts_rels_authors_id_idx" ON "posts_rels" USING btree ("authors_id"); + CREATE INDEX IF NOT EXISTS "posts_rels_tags_id_idx" ON "posts_rels" USING btree ("tags_id"); + CREATE INDEX IF NOT EXISTS "_posts_v_parent_idx" ON "_posts_v" USING btree ("parent_id"); + CREATE INDEX IF NOT EXISTS "_posts_v_version_version_slug_idx" ON "_posts_v" USING btree ("version_slug"); + CREATE INDEX IF NOT EXISTS "_posts_v_version_version_thumb_idx" ON "_posts_v" USING btree ("version_thumb_id"); + CREATE INDEX IF NOT EXISTS "_posts_v_version_version_image_idx" ON "_posts_v" USING btree ("version_image_id"); + CREATE INDEX IF NOT EXISTS "_posts_v_version_meta_version_meta_image_idx" ON "_posts_v" USING btree ("version_meta_image_id"); + CREATE INDEX IF NOT EXISTS "_posts_v_version_version_updated_at_idx" ON "_posts_v" USING btree ("version_updated_at"); + CREATE INDEX IF NOT EXISTS "_posts_v_version_version_created_at_idx" ON "_posts_v" USING btree ("version_created_at"); + CREATE INDEX IF NOT EXISTS "_posts_v_version_version__status_idx" ON "_posts_v" USING btree ("version__status"); + CREATE INDEX IF NOT EXISTS "_posts_v_created_at_idx" ON "_posts_v" USING btree ("created_at"); + CREATE INDEX IF NOT EXISTS "_posts_v_updated_at_idx" ON "_posts_v" USING btree ("updated_at"); + CREATE INDEX IF NOT EXISTS "_posts_v_latest_idx" ON "_posts_v" USING btree ("latest"); + CREATE INDEX IF NOT EXISTS "_posts_v_autosave_idx" ON "_posts_v" USING btree ("autosave"); + CREATE INDEX IF NOT EXISTS "_posts_v_rels_order_idx" ON "_posts_v_rels" USING btree ("order"); + CREATE INDEX IF NOT EXISTS "_posts_v_rels_parent_idx" ON "_posts_v_rels" USING btree ("parent_id"); + CREATE INDEX IF NOT EXISTS "_posts_v_rels_path_idx" ON "_posts_v_rels" USING btree ("path"); + CREATE INDEX IF NOT EXISTS "_posts_v_rels_categories_id_idx" ON "_posts_v_rels" USING btree ("categories_id"); + CREATE INDEX IF NOT EXISTS "_posts_v_rels_authors_id_idx" ON "_posts_v_rels" USING btree ("authors_id"); + CREATE INDEX IF NOT EXISTS "_posts_v_rels_tags_id_idx" ON "_posts_v_rels" USING btree ("tags_id"); + CREATE INDEX IF NOT EXISTS "tags_updated_at_idx" ON "tags" USING btree ("updated_at"); + CREATE INDEX IF NOT EXISTS "tags_created_at_idx" ON "tags" USING btree ("created_at"); + CREATE INDEX IF NOT EXISTS "users_roles_order_idx" ON "users_roles" USING btree ("order"); + CREATE INDEX IF NOT EXISTS "users_roles_parent_idx" ON "users_roles" USING btree ("parent_id"); + CREATE INDEX IF NOT EXISTS "users_updated_at_idx" ON "users" USING btree ("updated_at"); + CREATE INDEX IF NOT EXISTS "users_created_at_idx" ON "users" USING btree ("created_at"); + CREATE UNIQUE INDEX IF NOT EXISTS "users_email_idx" ON "users" USING btree ("email"); + CREATE INDEX IF NOT EXISTS "payload_jobs_log_order_idx" ON "payload_jobs_log" USING btree ("_order"); + CREATE INDEX IF NOT EXISTS "payload_jobs_log_parent_id_idx" ON "payload_jobs_log" USING btree ("_parent_id"); + CREATE INDEX IF NOT EXISTS "payload_jobs_completed_at_idx" ON "payload_jobs" USING btree ("completed_at"); + CREATE INDEX IF NOT EXISTS "payload_jobs_total_tried_idx" ON "payload_jobs" USING btree ("total_tried"); + CREATE INDEX IF NOT EXISTS "payload_jobs_has_error_idx" ON "payload_jobs" USING btree ("has_error"); + CREATE INDEX IF NOT EXISTS "payload_jobs_task_slug_idx" ON "payload_jobs" USING btree ("task_slug"); + CREATE INDEX IF NOT EXISTS "payload_jobs_queue_idx" ON "payload_jobs" USING btree ("queue"); + CREATE INDEX IF NOT EXISTS "payload_jobs_wait_until_idx" ON "payload_jobs" USING btree ("wait_until"); + CREATE INDEX IF NOT EXISTS "payload_jobs_processing_idx" ON "payload_jobs" USING btree ("processing"); + CREATE INDEX IF NOT EXISTS "payload_jobs_updated_at_idx" ON "payload_jobs" USING btree ("updated_at"); + CREATE INDEX IF NOT EXISTS "payload_jobs_created_at_idx" ON "payload_jobs" USING btree ("created_at"); + CREATE INDEX IF NOT EXISTS "payload_locked_documents_global_slug_idx" ON "payload_locked_documents" USING btree ("global_slug"); + CREATE INDEX IF NOT EXISTS "payload_locked_documents_updated_at_idx" ON "payload_locked_documents" USING btree ("updated_at"); + CREATE INDEX IF NOT EXISTS "payload_locked_documents_created_at_idx" ON "payload_locked_documents" USING btree ("created_at"); + CREATE INDEX IF NOT EXISTS "payload_locked_documents_rels_order_idx" ON "payload_locked_documents_rels" USING btree ("order"); + CREATE INDEX IF NOT EXISTS "payload_locked_documents_rels_parent_idx" ON "payload_locked_documents_rels" USING btree ("parent_id"); + CREATE INDEX IF NOT EXISTS "payload_locked_documents_rels_path_idx" ON "payload_locked_documents_rels" USING btree ("path"); + CREATE INDEX IF NOT EXISTS "payload_locked_documents_rels_authors_id_idx" ON "payload_locked_documents_rels" USING btree ("authors_id"); + CREATE INDEX IF NOT EXISTS "payload_locked_documents_rels_categories_id_idx" ON "payload_locked_documents_rels" USING btree ("categories_id"); + CREATE INDEX IF NOT EXISTS "payload_locked_documents_rels_customers_id_idx" ON "payload_locked_documents_rels" USING btree ("customers_id"); + CREATE INDEX IF NOT EXISTS "payload_locked_documents_rels_events_id_idx" ON "payload_locked_documents_rels" USING btree ("events_id"); + CREATE INDEX IF NOT EXISTS "payload_locked_documents_rels_media_id_idx" ON "payload_locked_documents_rels" USING btree ("media_id"); + CREATE INDEX IF NOT EXISTS "payload_locked_documents_rels_posts_id_idx" ON "payload_locked_documents_rels" USING btree ("posts_id"); + CREATE INDEX IF NOT EXISTS "payload_locked_documents_rels_tags_id_idx" ON "payload_locked_documents_rels" USING btree ("tags_id"); + CREATE INDEX IF NOT EXISTS "payload_locked_documents_rels_users_id_idx" ON "payload_locked_documents_rels" USING btree ("users_id"); + CREATE INDEX IF NOT EXISTS "payload_locked_documents_rels_payload_jobs_id_idx" ON "payload_locked_documents_rels" USING btree ("payload_jobs_id"); + CREATE INDEX IF NOT EXISTS "payload_preferences_key_idx" ON "payload_preferences" USING btree ("key"); + CREATE INDEX IF NOT EXISTS "payload_preferences_updated_at_idx" ON "payload_preferences" USING btree ("updated_at"); + CREATE INDEX IF NOT EXISTS "payload_preferences_created_at_idx" ON "payload_preferences" USING btree ("created_at"); + CREATE INDEX IF NOT EXISTS "payload_preferences_rels_order_idx" ON "payload_preferences_rels" USING btree ("order"); + CREATE INDEX IF NOT EXISTS "payload_preferences_rels_parent_idx" ON "payload_preferences_rels" USING btree ("parent_id"); + CREATE INDEX IF NOT EXISTS "payload_preferences_rels_path_idx" ON "payload_preferences_rels" USING btree ("path"); + CREATE INDEX IF NOT EXISTS "payload_preferences_rels_users_id_idx" ON "payload_preferences_rels" USING btree ("users_id"); + CREATE INDEX IF NOT EXISTS "payload_migrations_updated_at_idx" ON "payload_migrations" USING btree ("updated_at"); + CREATE INDEX IF NOT EXISTS "payload_migrations_created_at_idx" ON "payload_migrations" USING btree ("created_at");`) +} + +export async function down({ db, payload, req }: MigrateDownArgs): Promise { + await db.execute(sql` + DROP TABLE "authors" CASCADE; + DROP TABLE "categories_breadcrumbs" CASCADE; + DROP TABLE "categories" CASCADE; + DROP TABLE "customers_stats" CASCADE; + DROP TABLE "customers_misc" CASCADE; + DROP TABLE "customers_industry" CASCADE; + DROP TABLE "customers_supabase_products" CASCADE; + DROP TABLE "customers" CASCADE; + DROP TABLE "_customers_v_version_stats" CASCADE; + DROP TABLE "_customers_v_version_misc" CASCADE; + DROP TABLE "_customers_v_version_industry" CASCADE; + DROP TABLE "_customers_v_version_supabase_products" CASCADE; + DROP TABLE "_customers_v" CASCADE; + DROP TABLE "events_type" CASCADE; + DROP TABLE "events" CASCADE; + DROP TABLE "events_rels" CASCADE; + DROP TABLE "_events_v_version_type" CASCADE; + DROP TABLE "_events_v" CASCADE; + DROP TABLE "_events_v_rels" CASCADE; + DROP TABLE "media" CASCADE; + DROP TABLE "posts" CASCADE; + DROP TABLE "posts_rels" CASCADE; + DROP TABLE "_posts_v" CASCADE; + DROP TABLE "_posts_v_rels" CASCADE; + DROP TABLE "tags" CASCADE; + DROP TABLE "users_roles" CASCADE; + DROP TABLE "users" CASCADE; + DROP TABLE "payload_jobs_log" CASCADE; + DROP TABLE "payload_jobs" CASCADE; + DROP TABLE "payload_locked_documents" CASCADE; + DROP TABLE "payload_locked_documents_rels" CASCADE; + DROP TABLE "payload_preferences" CASCADE; + DROP TABLE "payload_preferences_rels" CASCADE; + DROP TABLE "payload_migrations" CASCADE; + DROP TYPE "public"."enum_customers_industry"; + DROP TYPE "public"."enum_customers_supabase_products"; + DROP TYPE "public"."enum_customers_company_size"; + DROP TYPE "public"."enum_customers_region"; + DROP TYPE "public"."enum_customers_status"; + DROP TYPE "public"."enum__customers_v_version_industry"; + DROP TYPE "public"."enum__customers_v_version_supabase_products"; + DROP TYPE "public"."enum__customers_v_version_company_size"; + DROP TYPE "public"."enum__customers_v_version_region"; + DROP TYPE "public"."enum__customers_v_version_status"; + DROP TYPE "public"."enum_events_type"; + DROP TYPE "public"."enum_events_timezone"; + DROP TYPE "public"."enum_events_link_target"; + DROP TYPE "public"."enum_events_main_cta_target"; + DROP TYPE "public"."enum_events_status"; + DROP TYPE "public"."enum__events_v_version_type"; + DROP TYPE "public"."enum__events_v_version_timezone"; + DROP TYPE "public"."enum__events_v_version_link_target"; + DROP TYPE "public"."enum__events_v_version_main_cta_target"; + DROP TYPE "public"."enum__events_v_version_status"; + DROP TYPE "public"."enum_posts_launchweek"; + DROP TYPE "public"."enum_posts_status"; + DROP TYPE "public"."enum__posts_v_version_launchweek"; + DROP TYPE "public"."enum__posts_v_version_status"; + DROP TYPE "public"."enum_users_roles"; + DROP TYPE "public"."enum_payload_jobs_log_task_slug"; + DROP TYPE "public"."enum_payload_jobs_log_state"; + DROP TYPE "public"."enum_payload_jobs_task_slug";`) +} diff --git a/apps/cms/src/migrations/20250619_160549.json b/apps/cms/src/migrations/20250619_160549.json new file mode 100644 index 00000000000..ef0494db176 --- /dev/null +++ b/apps/cms/src/migrations/20250619_160549.json @@ -0,0 +1,7330 @@ +{ + "id": "38bb76e1-daff-4133-8c6a-e751be8348e0", + "prevId": "00000000-0000-0000-0000-000000000000", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.authors": { + "name": "authors", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "author": { + "name": "author", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "author_id": { + "name": "author_id", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "position": { + "name": "position", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "author_url": { + "name": "author_url", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "author_image_url_id": { + "name": "author_image_url_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "username": { + "name": "username", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "created_at": { + "name": "created_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "authors_author_image_url_idx": { + "name": "authors_author_image_url_idx", + "columns": [ + { + "expression": "author_image_url_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "authors_updated_at_idx": { + "name": "authors_updated_at_idx", + "columns": [ + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "authors_created_at_idx": { + "name": "authors_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "authors_author_image_url_id_media_id_fk": { + "name": "authors_author_image_url_id_media_id_fk", + "tableFrom": "authors", + "tableTo": "media", + "columnsFrom": [ + "author_image_url_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.categories_breadcrumbs": { + "name": "categories_breadcrumbs", + "schema": "", + "columns": { + "_order": { + "name": "_order", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "_parent_id": { + "name": "_parent_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "id": { + "name": "id", + "type": "varchar", + "primaryKey": true, + "notNull": true + }, + "doc_id": { + "name": "doc_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "url": { + "name": "url", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "label": { + "name": "label", + "type": "varchar", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "categories_breadcrumbs_order_idx": { + "name": "categories_breadcrumbs_order_idx", + "columns": [ + { + "expression": "_order", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "categories_breadcrumbs_parent_id_idx": { + "name": "categories_breadcrumbs_parent_id_idx", + "columns": [ + { + "expression": "_parent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "categories_breadcrumbs_doc_idx": { + "name": "categories_breadcrumbs_doc_idx", + "columns": [ + { + "expression": "doc_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "categories_breadcrumbs_doc_id_categories_id_fk": { + "name": "categories_breadcrumbs_doc_id_categories_id_fk", + "tableFrom": "categories_breadcrumbs", + "tableTo": "categories", + "columnsFrom": [ + "doc_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "categories_breadcrumbs_parent_id_fk": { + "name": "categories_breadcrumbs_parent_id_fk", + "tableFrom": "categories_breadcrumbs", + "tableTo": "categories", + "columnsFrom": [ + "_parent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.categories": { + "name": "categories", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "parent_id": { + "name": "parent_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "created_at": { + "name": "created_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "categories_parent_idx": { + "name": "categories_parent_idx", + "columns": [ + { + "expression": "parent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "categories_updated_at_idx": { + "name": "categories_updated_at_idx", + "columns": [ + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "categories_created_at_idx": { + "name": "categories_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "categories_parent_id_categories_id_fk": { + "name": "categories_parent_id_categories_id_fk", + "tableFrom": "categories", + "tableTo": "categories", + "columnsFrom": [ + "parent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.customers_stats": { + "name": "customers_stats", + "schema": "", + "columns": { + "_order": { + "name": "_order", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "_parent_id": { + "name": "_parent_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "id": { + "name": "id", + "type": "varchar", + "primaryKey": true, + "notNull": true + }, + "stat": { + "name": "stat", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "label": { + "name": "label", + "type": "varchar", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "customers_stats_order_idx": { + "name": "customers_stats_order_idx", + "columns": [ + { + "expression": "_order", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "customers_stats_parent_id_idx": { + "name": "customers_stats_parent_id_idx", + "columns": [ + { + "expression": "_parent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "customers_stats_parent_id_fk": { + "name": "customers_stats_parent_id_fk", + "tableFrom": "customers_stats", + "tableTo": "customers", + "columnsFrom": [ + "_parent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.customers_misc": { + "name": "customers_misc", + "schema": "", + "columns": { + "_order": { + "name": "_order", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "_parent_id": { + "name": "_parent_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "id": { + "name": "id", + "type": "varchar", + "primaryKey": true, + "notNull": true + }, + "label": { + "name": "label", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "text": { + "name": "text", + "type": "varchar", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "customers_misc_order_idx": { + "name": "customers_misc_order_idx", + "columns": [ + { + "expression": "_order", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "customers_misc_parent_id_idx": { + "name": "customers_misc_parent_id_idx", + "columns": [ + { + "expression": "_parent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "customers_misc_parent_id_fk": { + "name": "customers_misc_parent_id_fk", + "tableFrom": "customers_misc", + "tableTo": "customers", + "columnsFrom": [ + "_parent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.customers_industry": { + "name": "customers_industry", + "schema": "", + "columns": { + "order": { + "name": "order", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "parent_id": { + "name": "parent_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "value": { + "name": "value", + "type": "enum_customers_industry", + "typeSchema": "public", + "primaryKey": false, + "notNull": false + }, + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + } + }, + "indexes": { + "customers_industry_order_idx": { + "name": "customers_industry_order_idx", + "columns": [ + { + "expression": "order", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "customers_industry_parent_idx": { + "name": "customers_industry_parent_idx", + "columns": [ + { + "expression": "parent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "customers_industry_parent_fk": { + "name": "customers_industry_parent_fk", + "tableFrom": "customers_industry", + "tableTo": "customers", + "columnsFrom": [ + "parent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.customers_supabase_products": { + "name": "customers_supabase_products", + "schema": "", + "columns": { + "order": { + "name": "order", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "parent_id": { + "name": "parent_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "value": { + "name": "value", + "type": "enum_customers_supabase_products", + "typeSchema": "public", + "primaryKey": false, + "notNull": false + }, + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + } + }, + "indexes": { + "customers_supabase_products_order_idx": { + "name": "customers_supabase_products_order_idx", + "columns": [ + { + "expression": "order", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "customers_supabase_products_parent_idx": { + "name": "customers_supabase_products_parent_idx", + "columns": [ + { + "expression": "parent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "customers_supabase_products_parent_fk": { + "name": "customers_supabase_products_parent_fk", + "tableFrom": "customers_supabase_products", + "tableTo": "customers", + "columnsFrom": [ + "parent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.customers": { + "name": "customers", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "title": { + "name": "title", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "slug": { + "name": "slug", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "slug_lock": { + "name": "slug_lock", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": true + }, + "content": { + "name": "content", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "description": { + "name": "description", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "about": { + "name": "about", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "company_url": { + "name": "company_url", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "company_size": { + "name": "company_size", + "type": "enum_customers_company_size", + "typeSchema": "public", + "primaryKey": false, + "notNull": false + }, + "region": { + "name": "region", + "type": "enum_customers_region", + "typeSchema": "public", + "primaryKey": false, + "notNull": false + }, + "meta_title": { + "name": "meta_title", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "meta_image_id": { + "name": "meta_image_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "meta_description": { + "name": "meta_description", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "published_at": { + "name": "published_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": false + }, + "logo_id": { + "name": "logo_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "logo_inverse_id": { + "name": "logo_inverse_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "created_at": { + "name": "created_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "_status": { + "name": "_status", + "type": "enum_customers_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": false, + "default": "'draft'" + } + }, + "indexes": { + "customers_slug_idx": { + "name": "customers_slug_idx", + "columns": [ + { + "expression": "slug", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "customers_meta_meta_image_idx": { + "name": "customers_meta_meta_image_idx", + "columns": [ + { + "expression": "meta_image_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "customers_logo_idx": { + "name": "customers_logo_idx", + "columns": [ + { + "expression": "logo_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "customers_logo_inverse_idx": { + "name": "customers_logo_inverse_idx", + "columns": [ + { + "expression": "logo_inverse_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "customers_updated_at_idx": { + "name": "customers_updated_at_idx", + "columns": [ + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "customers_created_at_idx": { + "name": "customers_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "customers__status_idx": { + "name": "customers__status_idx", + "columns": [ + { + "expression": "_status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "customers_meta_image_id_media_id_fk": { + "name": "customers_meta_image_id_media_id_fk", + "tableFrom": "customers", + "tableTo": "media", + "columnsFrom": [ + "meta_image_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "customers_logo_id_media_id_fk": { + "name": "customers_logo_id_media_id_fk", + "tableFrom": "customers", + "tableTo": "media", + "columnsFrom": [ + "logo_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "customers_logo_inverse_id_media_id_fk": { + "name": "customers_logo_inverse_id_media_id_fk", + "tableFrom": "customers", + "tableTo": "media", + "columnsFrom": [ + "logo_inverse_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public._customers_v_version_stats": { + "name": "_customers_v_version_stats", + "schema": "", + "columns": { + "_order": { + "name": "_order", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "_parent_id": { + "name": "_parent_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "stat": { + "name": "stat", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "label": { + "name": "label", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "_uuid": { + "name": "_uuid", + "type": "varchar", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "_customers_v_version_stats_order_idx": { + "name": "_customers_v_version_stats_order_idx", + "columns": [ + { + "expression": "_order", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "_customers_v_version_stats_parent_id_idx": { + "name": "_customers_v_version_stats_parent_id_idx", + "columns": [ + { + "expression": "_parent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "_customers_v_version_stats_parent_id_fk": { + "name": "_customers_v_version_stats_parent_id_fk", + "tableFrom": "_customers_v_version_stats", + "tableTo": "_customers_v", + "columnsFrom": [ + "_parent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public._customers_v_version_misc": { + "name": "_customers_v_version_misc", + "schema": "", + "columns": { + "_order": { + "name": "_order", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "_parent_id": { + "name": "_parent_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "label": { + "name": "label", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "text": { + "name": "text", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "_uuid": { + "name": "_uuid", + "type": "varchar", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "_customers_v_version_misc_order_idx": { + "name": "_customers_v_version_misc_order_idx", + "columns": [ + { + "expression": "_order", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "_customers_v_version_misc_parent_id_idx": { + "name": "_customers_v_version_misc_parent_id_idx", + "columns": [ + { + "expression": "_parent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "_customers_v_version_misc_parent_id_fk": { + "name": "_customers_v_version_misc_parent_id_fk", + "tableFrom": "_customers_v_version_misc", + "tableTo": "_customers_v", + "columnsFrom": [ + "_parent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public._customers_v_version_industry": { + "name": "_customers_v_version_industry", + "schema": "", + "columns": { + "order": { + "name": "order", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "parent_id": { + "name": "parent_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "value": { + "name": "value", + "type": "enum__customers_v_version_industry", + "typeSchema": "public", + "primaryKey": false, + "notNull": false + }, + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + } + }, + "indexes": { + "_customers_v_version_industry_order_idx": { + "name": "_customers_v_version_industry_order_idx", + "columns": [ + { + "expression": "order", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "_customers_v_version_industry_parent_idx": { + "name": "_customers_v_version_industry_parent_idx", + "columns": [ + { + "expression": "parent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "_customers_v_version_industry_parent_fk": { + "name": "_customers_v_version_industry_parent_fk", + "tableFrom": "_customers_v_version_industry", + "tableTo": "_customers_v", + "columnsFrom": [ + "parent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public._customers_v_version_supabase_products": { + "name": "_customers_v_version_supabase_products", + "schema": "", + "columns": { + "order": { + "name": "order", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "parent_id": { + "name": "parent_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "value": { + "name": "value", + "type": "enum__customers_v_version_supabase_products", + "typeSchema": "public", + "primaryKey": false, + "notNull": false + }, + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + } + }, + "indexes": { + "_customers_v_version_supabase_products_order_idx": { + "name": "_customers_v_version_supabase_products_order_idx", + "columns": [ + { + "expression": "order", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "_customers_v_version_supabase_products_parent_idx": { + "name": "_customers_v_version_supabase_products_parent_idx", + "columns": [ + { + "expression": "parent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "_customers_v_version_supabase_products_parent_fk": { + "name": "_customers_v_version_supabase_products_parent_fk", + "tableFrom": "_customers_v_version_supabase_products", + "tableTo": "_customers_v", + "columnsFrom": [ + "parent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public._customers_v": { + "name": "_customers_v", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "parent_id": { + "name": "parent_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "version_name": { + "name": "version_name", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "version_title": { + "name": "version_title", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "version_slug": { + "name": "version_slug", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "version_slug_lock": { + "name": "version_slug_lock", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": true + }, + "version_content": { + "name": "version_content", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "version_description": { + "name": "version_description", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "version_about": { + "name": "version_about", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "version_company_url": { + "name": "version_company_url", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "version_company_size": { + "name": "version_company_size", + "type": "enum__customers_v_version_company_size", + "typeSchema": "public", + "primaryKey": false, + "notNull": false + }, + "version_region": { + "name": "version_region", + "type": "enum__customers_v_version_region", + "typeSchema": "public", + "primaryKey": false, + "notNull": false + }, + "version_meta_title": { + "name": "version_meta_title", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "version_meta_image_id": { + "name": "version_meta_image_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "version_meta_description": { + "name": "version_meta_description", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "version_published_at": { + "name": "version_published_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": false + }, + "version_logo_id": { + "name": "version_logo_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "version_logo_inverse_id": { + "name": "version_logo_inverse_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "version_updated_at": { + "name": "version_updated_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": false + }, + "version_created_at": { + "name": "version_created_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": false + }, + "version__status": { + "name": "version__status", + "type": "enum__customers_v_version_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": false, + "default": "'draft'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "latest": { + "name": "latest", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "autosave": { + "name": "autosave", + "type": "boolean", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "_customers_v_parent_idx": { + "name": "_customers_v_parent_idx", + "columns": [ + { + "expression": "parent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "_customers_v_version_version_slug_idx": { + "name": "_customers_v_version_version_slug_idx", + "columns": [ + { + "expression": "version_slug", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "_customers_v_version_meta_version_meta_image_idx": { + "name": "_customers_v_version_meta_version_meta_image_idx", + "columns": [ + { + "expression": "version_meta_image_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "_customers_v_version_version_logo_idx": { + "name": "_customers_v_version_version_logo_idx", + "columns": [ + { + "expression": "version_logo_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "_customers_v_version_version_logo_inverse_idx": { + "name": "_customers_v_version_version_logo_inverse_idx", + "columns": [ + { + "expression": "version_logo_inverse_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "_customers_v_version_version_updated_at_idx": { + "name": "_customers_v_version_version_updated_at_idx", + "columns": [ + { + "expression": "version_updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "_customers_v_version_version_created_at_idx": { + "name": "_customers_v_version_version_created_at_idx", + "columns": [ + { + "expression": "version_created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "_customers_v_version_version__status_idx": { + "name": "_customers_v_version_version__status_idx", + "columns": [ + { + "expression": "version__status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "_customers_v_created_at_idx": { + "name": "_customers_v_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "_customers_v_updated_at_idx": { + "name": "_customers_v_updated_at_idx", + "columns": [ + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "_customers_v_latest_idx": { + "name": "_customers_v_latest_idx", + "columns": [ + { + "expression": "latest", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "_customers_v_autosave_idx": { + "name": "_customers_v_autosave_idx", + "columns": [ + { + "expression": "autosave", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "_customers_v_parent_id_customers_id_fk": { + "name": "_customers_v_parent_id_customers_id_fk", + "tableFrom": "_customers_v", + "tableTo": "customers", + "columnsFrom": [ + "parent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "_customers_v_version_meta_image_id_media_id_fk": { + "name": "_customers_v_version_meta_image_id_media_id_fk", + "tableFrom": "_customers_v", + "tableTo": "media", + "columnsFrom": [ + "version_meta_image_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "_customers_v_version_logo_id_media_id_fk": { + "name": "_customers_v_version_logo_id_media_id_fk", + "tableFrom": "_customers_v", + "tableTo": "media", + "columnsFrom": [ + "version_logo_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "_customers_v_version_logo_inverse_id_media_id_fk": { + "name": "_customers_v_version_logo_inverse_id_media_id_fk", + "tableFrom": "_customers_v", + "tableTo": "media", + "columnsFrom": [ + "version_logo_inverse_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.events_type": { + "name": "events_type", + "schema": "", + "columns": { + "order": { + "name": "order", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "parent_id": { + "name": "parent_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "value": { + "name": "value", + "type": "enum_events_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": false + }, + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + } + }, + "indexes": { + "events_type_order_idx": { + "name": "events_type_order_idx", + "columns": [ + { + "expression": "order", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "events_type_parent_idx": { + "name": "events_type_parent_idx", + "columns": [ + { + "expression": "parent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "events_type_parent_fk": { + "name": "events_type_parent_fk", + "tableFrom": "events_type", + "tableTo": "events", + "columnsFrom": [ + "parent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.events": { + "name": "events", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "title": { + "name": "title", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "slug": { + "name": "slug", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "slug_lock": { + "name": "slug_lock", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": true + }, + "subtitle": { + "name": "subtitle", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "content": { + "name": "content", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "thumb_id": { + "name": "thumb_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "image_id": { + "name": "image_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "date": { + "name": "date", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": false + }, + "timezone": { + "name": "timezone", + "type": "enum_events_timezone", + "typeSchema": "public", + "primaryKey": false, + "notNull": false + }, + "show_end_date": { + "name": "show_end_date", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "end_date": { + "name": "end_date", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": false + }, + "duration": { + "name": "duration", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "on_demand": { + "name": "on_demand", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "disable_page_build": { + "name": "disable_page_build", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "link_href": { + "name": "link_href", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "link_target": { + "name": "link_target", + "type": "enum_events_link_target", + "typeSchema": "public", + "primaryKey": false, + "notNull": false, + "default": "'_blank'" + }, + "main_cta_href": { + "name": "main_cta_href", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "main_cta_target": { + "name": "main_cta_target", + "type": "enum_events_main_cta_target", + "typeSchema": "public", + "primaryKey": false, + "notNull": false, + "default": "'_blank'" + }, + "main_cta_label": { + "name": "main_cta_label", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "main_cta_disabled": { + "name": "main_cta_disabled", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "main_cta_disabled_label": { + "name": "main_cta_disabled_label", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "company_show_company": { + "name": "company_show_company", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "company_name": { + "name": "company_name", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "company_website_url": { + "name": "company_website_url", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "company_logo_id": { + "name": "company_logo_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "company_logo_light_id": { + "name": "company_logo_light_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "participants_show_participants": { + "name": "participants_show_participants", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "meta_title": { + "name": "meta_title", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "meta_image_id": { + "name": "meta_image_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "meta_description": { + "name": "meta_description", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "published_at": { + "name": "published_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "created_at": { + "name": "created_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "_status": { + "name": "_status", + "type": "enum_events_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": false, + "default": "'draft'" + } + }, + "indexes": { + "events_slug_idx": { + "name": "events_slug_idx", + "columns": [ + { + "expression": "slug", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "events_thumb_idx": { + "name": "events_thumb_idx", + "columns": [ + { + "expression": "thumb_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "events_image_idx": { + "name": "events_image_idx", + "columns": [ + { + "expression": "image_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "events_company_company_logo_idx": { + "name": "events_company_company_logo_idx", + "columns": [ + { + "expression": "company_logo_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "events_company_company_logo_light_idx": { + "name": "events_company_company_logo_light_idx", + "columns": [ + { + "expression": "company_logo_light_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "events_meta_meta_image_idx": { + "name": "events_meta_meta_image_idx", + "columns": [ + { + "expression": "meta_image_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "events_updated_at_idx": { + "name": "events_updated_at_idx", + "columns": [ + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "events_created_at_idx": { + "name": "events_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "events__status_idx": { + "name": "events__status_idx", + "columns": [ + { + "expression": "_status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "events_thumb_id_media_id_fk": { + "name": "events_thumb_id_media_id_fk", + "tableFrom": "events", + "tableTo": "media", + "columnsFrom": [ + "thumb_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "events_image_id_media_id_fk": { + "name": "events_image_id_media_id_fk", + "tableFrom": "events", + "tableTo": "media", + "columnsFrom": [ + "image_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "events_company_logo_id_media_id_fk": { + "name": "events_company_logo_id_media_id_fk", + "tableFrom": "events", + "tableTo": "media", + "columnsFrom": [ + "company_logo_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "events_company_logo_light_id_media_id_fk": { + "name": "events_company_logo_light_id_media_id_fk", + "tableFrom": "events", + "tableTo": "media", + "columnsFrom": [ + "company_logo_light_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "events_meta_image_id_media_id_fk": { + "name": "events_meta_image_id_media_id_fk", + "tableFrom": "events", + "tableTo": "media", + "columnsFrom": [ + "meta_image_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.events_rels": { + "name": "events_rels", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "order": { + "name": "order", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "parent_id": { + "name": "parent_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "path": { + "name": "path", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "authors_id": { + "name": "authors_id", + "type": "integer", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "events_rels_order_idx": { + "name": "events_rels_order_idx", + "columns": [ + { + "expression": "order", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "events_rels_parent_idx": { + "name": "events_rels_parent_idx", + "columns": [ + { + "expression": "parent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "events_rels_path_idx": { + "name": "events_rels_path_idx", + "columns": [ + { + "expression": "path", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "events_rels_authors_id_idx": { + "name": "events_rels_authors_id_idx", + "columns": [ + { + "expression": "authors_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "events_rels_parent_fk": { + "name": "events_rels_parent_fk", + "tableFrom": "events_rels", + "tableTo": "events", + "columnsFrom": [ + "parent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "events_rels_authors_fk": { + "name": "events_rels_authors_fk", + "tableFrom": "events_rels", + "tableTo": "authors", + "columnsFrom": [ + "authors_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public._events_v_version_type": { + "name": "_events_v_version_type", + "schema": "", + "columns": { + "order": { + "name": "order", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "parent_id": { + "name": "parent_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "value": { + "name": "value", + "type": "enum__events_v_version_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": false + }, + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + } + }, + "indexes": { + "_events_v_version_type_order_idx": { + "name": "_events_v_version_type_order_idx", + "columns": [ + { + "expression": "order", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "_events_v_version_type_parent_idx": { + "name": "_events_v_version_type_parent_idx", + "columns": [ + { + "expression": "parent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "_events_v_version_type_parent_fk": { + "name": "_events_v_version_type_parent_fk", + "tableFrom": "_events_v_version_type", + "tableTo": "_events_v", + "columnsFrom": [ + "parent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public._events_v": { + "name": "_events_v", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "parent_id": { + "name": "parent_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "version_title": { + "name": "version_title", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "version_slug": { + "name": "version_slug", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "version_slug_lock": { + "name": "version_slug_lock", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": true + }, + "version_subtitle": { + "name": "version_subtitle", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "version_content": { + "name": "version_content", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "version_thumb_id": { + "name": "version_thumb_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "version_image_id": { + "name": "version_image_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "version_date": { + "name": "version_date", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": false + }, + "version_timezone": { + "name": "version_timezone", + "type": "enum__events_v_version_timezone", + "typeSchema": "public", + "primaryKey": false, + "notNull": false + }, + "version_show_end_date": { + "name": "version_show_end_date", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "version_end_date": { + "name": "version_end_date", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": false + }, + "version_duration": { + "name": "version_duration", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "version_on_demand": { + "name": "version_on_demand", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "version_disable_page_build": { + "name": "version_disable_page_build", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "version_link_href": { + "name": "version_link_href", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "version_link_target": { + "name": "version_link_target", + "type": "enum__events_v_version_link_target", + "typeSchema": "public", + "primaryKey": false, + "notNull": false, + "default": "'_blank'" + }, + "version_main_cta_href": { + "name": "version_main_cta_href", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "version_main_cta_target": { + "name": "version_main_cta_target", + "type": "enum__events_v_version_main_cta_target", + "typeSchema": "public", + "primaryKey": false, + "notNull": false, + "default": "'_blank'" + }, + "version_main_cta_label": { + "name": "version_main_cta_label", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "version_main_cta_disabled": { + "name": "version_main_cta_disabled", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "version_main_cta_disabled_label": { + "name": "version_main_cta_disabled_label", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "version_company_show_company": { + "name": "version_company_show_company", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "version_company_name": { + "name": "version_company_name", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "version_company_website_url": { + "name": "version_company_website_url", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "version_company_logo_id": { + "name": "version_company_logo_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "version_company_logo_light_id": { + "name": "version_company_logo_light_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "version_participants_show_participants": { + "name": "version_participants_show_participants", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "version_meta_title": { + "name": "version_meta_title", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "version_meta_image_id": { + "name": "version_meta_image_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "version_meta_description": { + "name": "version_meta_description", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "version_published_at": { + "name": "version_published_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": false + }, + "version_updated_at": { + "name": "version_updated_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": false + }, + "version_created_at": { + "name": "version_created_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": false + }, + "version__status": { + "name": "version__status", + "type": "enum__events_v_version_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": false, + "default": "'draft'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "latest": { + "name": "latest", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "autosave": { + "name": "autosave", + "type": "boolean", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "_events_v_parent_idx": { + "name": "_events_v_parent_idx", + "columns": [ + { + "expression": "parent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "_events_v_version_version_slug_idx": { + "name": "_events_v_version_version_slug_idx", + "columns": [ + { + "expression": "version_slug", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "_events_v_version_version_thumb_idx": { + "name": "_events_v_version_version_thumb_idx", + "columns": [ + { + "expression": "version_thumb_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "_events_v_version_version_image_idx": { + "name": "_events_v_version_version_image_idx", + "columns": [ + { + "expression": "version_image_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "_events_v_version_company_version_company_logo_idx": { + "name": "_events_v_version_company_version_company_logo_idx", + "columns": [ + { + "expression": "version_company_logo_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "_events_v_version_company_version_company_logo_light_idx": { + "name": "_events_v_version_company_version_company_logo_light_idx", + "columns": [ + { + "expression": "version_company_logo_light_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "_events_v_version_meta_version_meta_image_idx": { + "name": "_events_v_version_meta_version_meta_image_idx", + "columns": [ + { + "expression": "version_meta_image_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "_events_v_version_version_updated_at_idx": { + "name": "_events_v_version_version_updated_at_idx", + "columns": [ + { + "expression": "version_updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "_events_v_version_version_created_at_idx": { + "name": "_events_v_version_version_created_at_idx", + "columns": [ + { + "expression": "version_created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "_events_v_version_version__status_idx": { + "name": "_events_v_version_version__status_idx", + "columns": [ + { + "expression": "version__status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "_events_v_created_at_idx": { + "name": "_events_v_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "_events_v_updated_at_idx": { + "name": "_events_v_updated_at_idx", + "columns": [ + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "_events_v_latest_idx": { + "name": "_events_v_latest_idx", + "columns": [ + { + "expression": "latest", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "_events_v_autosave_idx": { + "name": "_events_v_autosave_idx", + "columns": [ + { + "expression": "autosave", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "_events_v_parent_id_events_id_fk": { + "name": "_events_v_parent_id_events_id_fk", + "tableFrom": "_events_v", + "tableTo": "events", + "columnsFrom": [ + "parent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "_events_v_version_thumb_id_media_id_fk": { + "name": "_events_v_version_thumb_id_media_id_fk", + "tableFrom": "_events_v", + "tableTo": "media", + "columnsFrom": [ + "version_thumb_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "_events_v_version_image_id_media_id_fk": { + "name": "_events_v_version_image_id_media_id_fk", + "tableFrom": "_events_v", + "tableTo": "media", + "columnsFrom": [ + "version_image_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "_events_v_version_company_logo_id_media_id_fk": { + "name": "_events_v_version_company_logo_id_media_id_fk", + "tableFrom": "_events_v", + "tableTo": "media", + "columnsFrom": [ + "version_company_logo_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "_events_v_version_company_logo_light_id_media_id_fk": { + "name": "_events_v_version_company_logo_light_id_media_id_fk", + "tableFrom": "_events_v", + "tableTo": "media", + "columnsFrom": [ + "version_company_logo_light_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "_events_v_version_meta_image_id_media_id_fk": { + "name": "_events_v_version_meta_image_id_media_id_fk", + "tableFrom": "_events_v", + "tableTo": "media", + "columnsFrom": [ + "version_meta_image_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public._events_v_rels": { + "name": "_events_v_rels", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "order": { + "name": "order", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "parent_id": { + "name": "parent_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "path": { + "name": "path", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "authors_id": { + "name": "authors_id", + "type": "integer", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "_events_v_rels_order_idx": { + "name": "_events_v_rels_order_idx", + "columns": [ + { + "expression": "order", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "_events_v_rels_parent_idx": { + "name": "_events_v_rels_parent_idx", + "columns": [ + { + "expression": "parent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "_events_v_rels_path_idx": { + "name": "_events_v_rels_path_idx", + "columns": [ + { + "expression": "path", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "_events_v_rels_authors_id_idx": { + "name": "_events_v_rels_authors_id_idx", + "columns": [ + { + "expression": "authors_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "_events_v_rels_parent_fk": { + "name": "_events_v_rels_parent_fk", + "tableFrom": "_events_v_rels", + "tableTo": "_events_v", + "columnsFrom": [ + "parent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "_events_v_rels_authors_fk": { + "name": "_events_v_rels_authors_fk", + "tableFrom": "_events_v_rels", + "tableTo": "authors", + "columnsFrom": [ + "authors_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.media": { + "name": "media", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "alt": { + "name": "alt", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "caption": { + "name": "caption", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "prefix": { + "name": "prefix", + "type": "varchar", + "primaryKey": false, + "notNull": false, + "default": "'media'" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "created_at": { + "name": "created_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "url": { + "name": "url", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "thumbnail_u_r_l": { + "name": "thumbnail_u_r_l", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "filename": { + "name": "filename", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "mime_type": { + "name": "mime_type", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "filesize": { + "name": "filesize", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "width": { + "name": "width", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "height": { + "name": "height", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "focal_x": { + "name": "focal_x", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "focal_y": { + "name": "focal_y", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "sizes_thumbnail_url": { + "name": "sizes_thumbnail_url", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "sizes_thumbnail_width": { + "name": "sizes_thumbnail_width", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "sizes_thumbnail_height": { + "name": "sizes_thumbnail_height", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "sizes_thumbnail_mime_type": { + "name": "sizes_thumbnail_mime_type", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "sizes_thumbnail_filesize": { + "name": "sizes_thumbnail_filesize", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "sizes_thumbnail_filename": { + "name": "sizes_thumbnail_filename", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "sizes_square_url": { + "name": "sizes_square_url", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "sizes_square_width": { + "name": "sizes_square_width", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "sizes_square_height": { + "name": "sizes_square_height", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "sizes_square_mime_type": { + "name": "sizes_square_mime_type", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "sizes_square_filesize": { + "name": "sizes_square_filesize", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "sizes_square_filename": { + "name": "sizes_square_filename", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "sizes_small_url": { + "name": "sizes_small_url", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "sizes_small_width": { + "name": "sizes_small_width", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "sizes_small_height": { + "name": "sizes_small_height", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "sizes_small_mime_type": { + "name": "sizes_small_mime_type", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "sizes_small_filesize": { + "name": "sizes_small_filesize", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "sizes_small_filename": { + "name": "sizes_small_filename", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "sizes_medium_url": { + "name": "sizes_medium_url", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "sizes_medium_width": { + "name": "sizes_medium_width", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "sizes_medium_height": { + "name": "sizes_medium_height", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "sizes_medium_mime_type": { + "name": "sizes_medium_mime_type", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "sizes_medium_filesize": { + "name": "sizes_medium_filesize", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "sizes_medium_filename": { + "name": "sizes_medium_filename", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "sizes_large_url": { + "name": "sizes_large_url", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "sizes_large_width": { + "name": "sizes_large_width", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "sizes_large_height": { + "name": "sizes_large_height", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "sizes_large_mime_type": { + "name": "sizes_large_mime_type", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "sizes_large_filesize": { + "name": "sizes_large_filesize", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "sizes_large_filename": { + "name": "sizes_large_filename", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "sizes_xlarge_url": { + "name": "sizes_xlarge_url", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "sizes_xlarge_width": { + "name": "sizes_xlarge_width", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "sizes_xlarge_height": { + "name": "sizes_xlarge_height", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "sizes_xlarge_mime_type": { + "name": "sizes_xlarge_mime_type", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "sizes_xlarge_filesize": { + "name": "sizes_xlarge_filesize", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "sizes_xlarge_filename": { + "name": "sizes_xlarge_filename", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "sizes_og_url": { + "name": "sizes_og_url", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "sizes_og_width": { + "name": "sizes_og_width", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "sizes_og_height": { + "name": "sizes_og_height", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "sizes_og_mime_type": { + "name": "sizes_og_mime_type", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "sizes_og_filesize": { + "name": "sizes_og_filesize", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "sizes_og_filename": { + "name": "sizes_og_filename", + "type": "varchar", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "media_updated_at_idx": { + "name": "media_updated_at_idx", + "columns": [ + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "media_created_at_idx": { + "name": "media_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "media_filename_idx": { + "name": "media_filename_idx", + "columns": [ + { + "expression": "filename", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "media_sizes_thumbnail_sizes_thumbnail_filename_idx": { + "name": "media_sizes_thumbnail_sizes_thumbnail_filename_idx", + "columns": [ + { + "expression": "sizes_thumbnail_filename", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "media_sizes_square_sizes_square_filename_idx": { + "name": "media_sizes_square_sizes_square_filename_idx", + "columns": [ + { + "expression": "sizes_square_filename", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "media_sizes_small_sizes_small_filename_idx": { + "name": "media_sizes_small_sizes_small_filename_idx", + "columns": [ + { + "expression": "sizes_small_filename", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "media_sizes_medium_sizes_medium_filename_idx": { + "name": "media_sizes_medium_sizes_medium_filename_idx", + "columns": [ + { + "expression": "sizes_medium_filename", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "media_sizes_large_sizes_large_filename_idx": { + "name": "media_sizes_large_sizes_large_filename_idx", + "columns": [ + { + "expression": "sizes_large_filename", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "media_sizes_xlarge_sizes_xlarge_filename_idx": { + "name": "media_sizes_xlarge_sizes_xlarge_filename_idx", + "columns": [ + { + "expression": "sizes_xlarge_filename", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "media_sizes_og_sizes_og_filename_idx": { + "name": "media_sizes_og_sizes_og_filename_idx", + "columns": [ + { + "expression": "sizes_og_filename", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.posts": { + "name": "posts", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "title": { + "name": "title", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "slug": { + "name": "slug", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "slug_lock": { + "name": "slug_lock", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": true + }, + "content": { + "name": "content", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "thumb_id": { + "name": "thumb_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "image_id": { + "name": "image_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "launchweek": { + "name": "launchweek", + "type": "enum_posts_launchweek", + "typeSchema": "public", + "primaryKey": false, + "notNull": false + }, + "reading_time": { + "name": "reading_time", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "date": { + "name": "date", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": false + }, + "toc_depth": { + "name": "toc_depth", + "type": "numeric", + "primaryKey": false, + "notNull": false, + "default": 2 + }, + "description": { + "name": "description", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "meta_title": { + "name": "meta_title", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "meta_image_id": { + "name": "meta_image_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "meta_description": { + "name": "meta_description", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "published_at": { + "name": "published_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "created_at": { + "name": "created_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "_status": { + "name": "_status", + "type": "enum_posts_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": false, + "default": "'draft'" + } + }, + "indexes": { + "posts_slug_idx": { + "name": "posts_slug_idx", + "columns": [ + { + "expression": "slug", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "posts_thumb_idx": { + "name": "posts_thumb_idx", + "columns": [ + { + "expression": "thumb_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "posts_image_idx": { + "name": "posts_image_idx", + "columns": [ + { + "expression": "image_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "posts_meta_meta_image_idx": { + "name": "posts_meta_meta_image_idx", + "columns": [ + { + "expression": "meta_image_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "posts_updated_at_idx": { + "name": "posts_updated_at_idx", + "columns": [ + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "posts_created_at_idx": { + "name": "posts_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "posts__status_idx": { + "name": "posts__status_idx", + "columns": [ + { + "expression": "_status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "posts_thumb_id_media_id_fk": { + "name": "posts_thumb_id_media_id_fk", + "tableFrom": "posts", + "tableTo": "media", + "columnsFrom": [ + "thumb_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "posts_image_id_media_id_fk": { + "name": "posts_image_id_media_id_fk", + "tableFrom": "posts", + "tableTo": "media", + "columnsFrom": [ + "image_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "posts_meta_image_id_media_id_fk": { + "name": "posts_meta_image_id_media_id_fk", + "tableFrom": "posts", + "tableTo": "media", + "columnsFrom": [ + "meta_image_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.posts_rels": { + "name": "posts_rels", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "order": { + "name": "order", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "parent_id": { + "name": "parent_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "path": { + "name": "path", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "categories_id": { + "name": "categories_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "authors_id": { + "name": "authors_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "tags_id": { + "name": "tags_id", + "type": "integer", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "posts_rels_order_idx": { + "name": "posts_rels_order_idx", + "columns": [ + { + "expression": "order", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "posts_rels_parent_idx": { + "name": "posts_rels_parent_idx", + "columns": [ + { + "expression": "parent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "posts_rels_path_idx": { + "name": "posts_rels_path_idx", + "columns": [ + { + "expression": "path", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "posts_rels_categories_id_idx": { + "name": "posts_rels_categories_id_idx", + "columns": [ + { + "expression": "categories_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "posts_rels_authors_id_idx": { + "name": "posts_rels_authors_id_idx", + "columns": [ + { + "expression": "authors_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "posts_rels_tags_id_idx": { + "name": "posts_rels_tags_id_idx", + "columns": [ + { + "expression": "tags_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "posts_rels_parent_fk": { + "name": "posts_rels_parent_fk", + "tableFrom": "posts_rels", + "tableTo": "posts", + "columnsFrom": [ + "parent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "posts_rels_categories_fk": { + "name": "posts_rels_categories_fk", + "tableFrom": "posts_rels", + "tableTo": "categories", + "columnsFrom": [ + "categories_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "posts_rels_authors_fk": { + "name": "posts_rels_authors_fk", + "tableFrom": "posts_rels", + "tableTo": "authors", + "columnsFrom": [ + "authors_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "posts_rels_tags_fk": { + "name": "posts_rels_tags_fk", + "tableFrom": "posts_rels", + "tableTo": "tags", + "columnsFrom": [ + "tags_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public._posts_v": { + "name": "_posts_v", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "parent_id": { + "name": "parent_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "version_title": { + "name": "version_title", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "version_slug": { + "name": "version_slug", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "version_slug_lock": { + "name": "version_slug_lock", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": true + }, + "version_content": { + "name": "version_content", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "version_thumb_id": { + "name": "version_thumb_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "version_image_id": { + "name": "version_image_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "version_launchweek": { + "name": "version_launchweek", + "type": "enum__posts_v_version_launchweek", + "typeSchema": "public", + "primaryKey": false, + "notNull": false + }, + "version_reading_time": { + "name": "version_reading_time", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "version_date": { + "name": "version_date", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": false + }, + "version_toc_depth": { + "name": "version_toc_depth", + "type": "numeric", + "primaryKey": false, + "notNull": false, + "default": 2 + }, + "version_description": { + "name": "version_description", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "version_meta_title": { + "name": "version_meta_title", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "version_meta_image_id": { + "name": "version_meta_image_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "version_meta_description": { + "name": "version_meta_description", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "version_published_at": { + "name": "version_published_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": false + }, + "version_updated_at": { + "name": "version_updated_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": false + }, + "version_created_at": { + "name": "version_created_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": false + }, + "version__status": { + "name": "version__status", + "type": "enum__posts_v_version_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": false, + "default": "'draft'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "latest": { + "name": "latest", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "autosave": { + "name": "autosave", + "type": "boolean", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "_posts_v_parent_idx": { + "name": "_posts_v_parent_idx", + "columns": [ + { + "expression": "parent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "_posts_v_version_version_slug_idx": { + "name": "_posts_v_version_version_slug_idx", + "columns": [ + { + "expression": "version_slug", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "_posts_v_version_version_thumb_idx": { + "name": "_posts_v_version_version_thumb_idx", + "columns": [ + { + "expression": "version_thumb_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "_posts_v_version_version_image_idx": { + "name": "_posts_v_version_version_image_idx", + "columns": [ + { + "expression": "version_image_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "_posts_v_version_meta_version_meta_image_idx": { + "name": "_posts_v_version_meta_version_meta_image_idx", + "columns": [ + { + "expression": "version_meta_image_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "_posts_v_version_version_updated_at_idx": { + "name": "_posts_v_version_version_updated_at_idx", + "columns": [ + { + "expression": "version_updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "_posts_v_version_version_created_at_idx": { + "name": "_posts_v_version_version_created_at_idx", + "columns": [ + { + "expression": "version_created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "_posts_v_version_version__status_idx": { + "name": "_posts_v_version_version__status_idx", + "columns": [ + { + "expression": "version__status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "_posts_v_created_at_idx": { + "name": "_posts_v_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "_posts_v_updated_at_idx": { + "name": "_posts_v_updated_at_idx", + "columns": [ + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "_posts_v_latest_idx": { + "name": "_posts_v_latest_idx", + "columns": [ + { + "expression": "latest", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "_posts_v_autosave_idx": { + "name": "_posts_v_autosave_idx", + "columns": [ + { + "expression": "autosave", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "_posts_v_parent_id_posts_id_fk": { + "name": "_posts_v_parent_id_posts_id_fk", + "tableFrom": "_posts_v", + "tableTo": "posts", + "columnsFrom": [ + "parent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "_posts_v_version_thumb_id_media_id_fk": { + "name": "_posts_v_version_thumb_id_media_id_fk", + "tableFrom": "_posts_v", + "tableTo": "media", + "columnsFrom": [ + "version_thumb_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "_posts_v_version_image_id_media_id_fk": { + "name": "_posts_v_version_image_id_media_id_fk", + "tableFrom": "_posts_v", + "tableTo": "media", + "columnsFrom": [ + "version_image_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "_posts_v_version_meta_image_id_media_id_fk": { + "name": "_posts_v_version_meta_image_id_media_id_fk", + "tableFrom": "_posts_v", + "tableTo": "media", + "columnsFrom": [ + "version_meta_image_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public._posts_v_rels": { + "name": "_posts_v_rels", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "order": { + "name": "order", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "parent_id": { + "name": "parent_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "path": { + "name": "path", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "categories_id": { + "name": "categories_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "authors_id": { + "name": "authors_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "tags_id": { + "name": "tags_id", + "type": "integer", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "_posts_v_rels_order_idx": { + "name": "_posts_v_rels_order_idx", + "columns": [ + { + "expression": "order", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "_posts_v_rels_parent_idx": { + "name": "_posts_v_rels_parent_idx", + "columns": [ + { + "expression": "parent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "_posts_v_rels_path_idx": { + "name": "_posts_v_rels_path_idx", + "columns": [ + { + "expression": "path", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "_posts_v_rels_categories_id_idx": { + "name": "_posts_v_rels_categories_id_idx", + "columns": [ + { + "expression": "categories_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "_posts_v_rels_authors_id_idx": { + "name": "_posts_v_rels_authors_id_idx", + "columns": [ + { + "expression": "authors_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "_posts_v_rels_tags_id_idx": { + "name": "_posts_v_rels_tags_id_idx", + "columns": [ + { + "expression": "tags_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "_posts_v_rels_parent_fk": { + "name": "_posts_v_rels_parent_fk", + "tableFrom": "_posts_v_rels", + "tableTo": "_posts_v", + "columnsFrom": [ + "parent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "_posts_v_rels_categories_fk": { + "name": "_posts_v_rels_categories_fk", + "tableFrom": "_posts_v_rels", + "tableTo": "categories", + "columnsFrom": [ + "categories_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "_posts_v_rels_authors_fk": { + "name": "_posts_v_rels_authors_fk", + "tableFrom": "_posts_v_rels", + "tableTo": "authors", + "columnsFrom": [ + "authors_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "_posts_v_rels_tags_fk": { + "name": "_posts_v_rels_tags_fk", + "tableFrom": "_posts_v_rels", + "tableTo": "tags", + "columnsFrom": [ + "tags_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.tags": { + "name": "tags", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "created_at": { + "name": "created_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "tags_updated_at_idx": { + "name": "tags_updated_at_idx", + "columns": [ + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "tags_created_at_idx": { + "name": "tags_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.users_roles": { + "name": "users_roles", + "schema": "", + "columns": { + "order": { + "name": "order", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "parent_id": { + "name": "parent_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "value": { + "name": "value", + "type": "enum_users_roles", + "typeSchema": "public", + "primaryKey": false, + "notNull": false + }, + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + } + }, + "indexes": { + "users_roles_order_idx": { + "name": "users_roles_order_idx", + "columns": [ + { + "expression": "order", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "users_roles_parent_idx": { + "name": "users_roles_parent_idx", + "columns": [ + { + "expression": "parent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "users_roles_parent_fk": { + "name": "users_roles_parent_fk", + "tableFrom": "users_roles", + "tableTo": "users", + "columnsFrom": [ + "parent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.users": { + "name": "users", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "created_at": { + "name": "created_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "email": { + "name": "email", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "reset_password_token": { + "name": "reset_password_token", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "reset_password_expiration": { + "name": "reset_password_expiration", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": false + }, + "salt": { + "name": "salt", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "hash": { + "name": "hash", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "login_attempts": { + "name": "login_attempts", + "type": "numeric", + "primaryKey": false, + "notNull": false, + "default": 0 + }, + "lock_until": { + "name": "lock_until", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "users_updated_at_idx": { + "name": "users_updated_at_idx", + "columns": [ + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "users_created_at_idx": { + "name": "users_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "users_email_idx": { + "name": "users_email_idx", + "columns": [ + { + "expression": "email", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.payload_jobs_log": { + "name": "payload_jobs_log", + "schema": "", + "columns": { + "_order": { + "name": "_order", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "_parent_id": { + "name": "_parent_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "id": { + "name": "id", + "type": "varchar", + "primaryKey": true, + "notNull": true + }, + "executed_at": { + "name": "executed_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": true + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": true + }, + "task_slug": { + "name": "task_slug", + "type": "enum_payload_jobs_log_task_slug", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "task_i_d": { + "name": "task_i_d", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "input": { + "name": "input", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "output": { + "name": "output", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "state": { + "name": "state", + "type": "enum_payload_jobs_log_state", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "error": { + "name": "error", + "type": "jsonb", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "payload_jobs_log_order_idx": { + "name": "payload_jobs_log_order_idx", + "columns": [ + { + "expression": "_order", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "payload_jobs_log_parent_id_idx": { + "name": "payload_jobs_log_parent_id_idx", + "columns": [ + { + "expression": "_parent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "payload_jobs_log_parent_id_fk": { + "name": "payload_jobs_log_parent_id_fk", + "tableFrom": "payload_jobs_log", + "tableTo": "payload_jobs", + "columnsFrom": [ + "_parent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.payload_jobs": { + "name": "payload_jobs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "input": { + "name": "input", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": false + }, + "total_tried": { + "name": "total_tried", + "type": "numeric", + "primaryKey": false, + "notNull": false, + "default": 0 + }, + "has_error": { + "name": "has_error", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "error": { + "name": "error", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "task_slug": { + "name": "task_slug", + "type": "enum_payload_jobs_task_slug", + "typeSchema": "public", + "primaryKey": false, + "notNull": false + }, + "queue": { + "name": "queue", + "type": "varchar", + "primaryKey": false, + "notNull": false, + "default": "'default'" + }, + "wait_until": { + "name": "wait_until", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": false + }, + "processing": { + "name": "processing", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "created_at": { + "name": "created_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "payload_jobs_completed_at_idx": { + "name": "payload_jobs_completed_at_idx", + "columns": [ + { + "expression": "completed_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "payload_jobs_total_tried_idx": { + "name": "payload_jobs_total_tried_idx", + "columns": [ + { + "expression": "total_tried", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "payload_jobs_has_error_idx": { + "name": "payload_jobs_has_error_idx", + "columns": [ + { + "expression": "has_error", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "payload_jobs_task_slug_idx": { + "name": "payload_jobs_task_slug_idx", + "columns": [ + { + "expression": "task_slug", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "payload_jobs_queue_idx": { + "name": "payload_jobs_queue_idx", + "columns": [ + { + "expression": "queue", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "payload_jobs_wait_until_idx": { + "name": "payload_jobs_wait_until_idx", + "columns": [ + { + "expression": "wait_until", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "payload_jobs_processing_idx": { + "name": "payload_jobs_processing_idx", + "columns": [ + { + "expression": "processing", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "payload_jobs_updated_at_idx": { + "name": "payload_jobs_updated_at_idx", + "columns": [ + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "payload_jobs_created_at_idx": { + "name": "payload_jobs_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.payload_locked_documents": { + "name": "payload_locked_documents", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "global_slug": { + "name": "global_slug", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "created_at": { + "name": "created_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "payload_locked_documents_global_slug_idx": { + "name": "payload_locked_documents_global_slug_idx", + "columns": [ + { + "expression": "global_slug", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "payload_locked_documents_updated_at_idx": { + "name": "payload_locked_documents_updated_at_idx", + "columns": [ + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "payload_locked_documents_created_at_idx": { + "name": "payload_locked_documents_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.payload_locked_documents_rels": { + "name": "payload_locked_documents_rels", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "order": { + "name": "order", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "parent_id": { + "name": "parent_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "path": { + "name": "path", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "authors_id": { + "name": "authors_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "categories_id": { + "name": "categories_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "customers_id": { + "name": "customers_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "events_id": { + "name": "events_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "media_id": { + "name": "media_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "posts_id": { + "name": "posts_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "tags_id": { + "name": "tags_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "users_id": { + "name": "users_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "payload_jobs_id": { + "name": "payload_jobs_id", + "type": "integer", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "payload_locked_documents_rels_order_idx": { + "name": "payload_locked_documents_rels_order_idx", + "columns": [ + { + "expression": "order", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "payload_locked_documents_rels_parent_idx": { + "name": "payload_locked_documents_rels_parent_idx", + "columns": [ + { + "expression": "parent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "payload_locked_documents_rels_path_idx": { + "name": "payload_locked_documents_rels_path_idx", + "columns": [ + { + "expression": "path", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "payload_locked_documents_rels_authors_id_idx": { + "name": "payload_locked_documents_rels_authors_id_idx", + "columns": [ + { + "expression": "authors_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "payload_locked_documents_rels_categories_id_idx": { + "name": "payload_locked_documents_rels_categories_id_idx", + "columns": [ + { + "expression": "categories_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "payload_locked_documents_rels_customers_id_idx": { + "name": "payload_locked_documents_rels_customers_id_idx", + "columns": [ + { + "expression": "customers_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "payload_locked_documents_rels_events_id_idx": { + "name": "payload_locked_documents_rels_events_id_idx", + "columns": [ + { + "expression": "events_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "payload_locked_documents_rels_media_id_idx": { + "name": "payload_locked_documents_rels_media_id_idx", + "columns": [ + { + "expression": "media_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "payload_locked_documents_rels_posts_id_idx": { + "name": "payload_locked_documents_rels_posts_id_idx", + "columns": [ + { + "expression": "posts_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "payload_locked_documents_rels_tags_id_idx": { + "name": "payload_locked_documents_rels_tags_id_idx", + "columns": [ + { + "expression": "tags_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "payload_locked_documents_rels_users_id_idx": { + "name": "payload_locked_documents_rels_users_id_idx", + "columns": [ + { + "expression": "users_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "payload_locked_documents_rels_payload_jobs_id_idx": { + "name": "payload_locked_documents_rels_payload_jobs_id_idx", + "columns": [ + { + "expression": "payload_jobs_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "payload_locked_documents_rels_parent_fk": { + "name": "payload_locked_documents_rels_parent_fk", + "tableFrom": "payload_locked_documents_rels", + "tableTo": "payload_locked_documents", + "columnsFrom": [ + "parent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "payload_locked_documents_rels_authors_fk": { + "name": "payload_locked_documents_rels_authors_fk", + "tableFrom": "payload_locked_documents_rels", + "tableTo": "authors", + "columnsFrom": [ + "authors_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "payload_locked_documents_rels_categories_fk": { + "name": "payload_locked_documents_rels_categories_fk", + "tableFrom": "payload_locked_documents_rels", + "tableTo": "categories", + "columnsFrom": [ + "categories_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "payload_locked_documents_rels_customers_fk": { + "name": "payload_locked_documents_rels_customers_fk", + "tableFrom": "payload_locked_documents_rels", + "tableTo": "customers", + "columnsFrom": [ + "customers_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "payload_locked_documents_rels_events_fk": { + "name": "payload_locked_documents_rels_events_fk", + "tableFrom": "payload_locked_documents_rels", + "tableTo": "events", + "columnsFrom": [ + "events_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "payload_locked_documents_rels_media_fk": { + "name": "payload_locked_documents_rels_media_fk", + "tableFrom": "payload_locked_documents_rels", + "tableTo": "media", + "columnsFrom": [ + "media_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "payload_locked_documents_rels_posts_fk": { + "name": "payload_locked_documents_rels_posts_fk", + "tableFrom": "payload_locked_documents_rels", + "tableTo": "posts", + "columnsFrom": [ + "posts_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "payload_locked_documents_rels_tags_fk": { + "name": "payload_locked_documents_rels_tags_fk", + "tableFrom": "payload_locked_documents_rels", + "tableTo": "tags", + "columnsFrom": [ + "tags_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "payload_locked_documents_rels_users_fk": { + "name": "payload_locked_documents_rels_users_fk", + "tableFrom": "payload_locked_documents_rels", + "tableTo": "users", + "columnsFrom": [ + "users_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "payload_locked_documents_rels_payload_jobs_fk": { + "name": "payload_locked_documents_rels_payload_jobs_fk", + "tableFrom": "payload_locked_documents_rels", + "tableTo": "payload_jobs", + "columnsFrom": [ + "payload_jobs_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.payload_preferences": { + "name": "payload_preferences", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "key": { + "name": "key", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "value": { + "name": "value", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "created_at": { + "name": "created_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "payload_preferences_key_idx": { + "name": "payload_preferences_key_idx", + "columns": [ + { + "expression": "key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "payload_preferences_updated_at_idx": { + "name": "payload_preferences_updated_at_idx", + "columns": [ + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "payload_preferences_created_at_idx": { + "name": "payload_preferences_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.payload_preferences_rels": { + "name": "payload_preferences_rels", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "order": { + "name": "order", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "parent_id": { + "name": "parent_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "path": { + "name": "path", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "users_id": { + "name": "users_id", + "type": "integer", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "payload_preferences_rels_order_idx": { + "name": "payload_preferences_rels_order_idx", + "columns": [ + { + "expression": "order", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "payload_preferences_rels_parent_idx": { + "name": "payload_preferences_rels_parent_idx", + "columns": [ + { + "expression": "parent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "payload_preferences_rels_path_idx": { + "name": "payload_preferences_rels_path_idx", + "columns": [ + { + "expression": "path", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "payload_preferences_rels_users_id_idx": { + "name": "payload_preferences_rels_users_id_idx", + "columns": [ + { + "expression": "users_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "payload_preferences_rels_parent_fk": { + "name": "payload_preferences_rels_parent_fk", + "tableFrom": "payload_preferences_rels", + "tableTo": "payload_preferences", + "columnsFrom": [ + "parent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "payload_preferences_rels_users_fk": { + "name": "payload_preferences_rels_users_fk", + "tableFrom": "payload_preferences_rels", + "tableTo": "users", + "columnsFrom": [ + "users_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.payload_migrations": { + "name": "payload_migrations", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "batch": { + "name": "batch", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "created_at": { + "name": "created_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "payload_migrations_updated_at_idx": { + "name": "payload_migrations_updated_at_idx", + "columns": [ + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "payload_migrations_created_at_idx": { + "name": "payload_migrations_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + } + }, + "enums": { + "public.enum_customers_industry": { + "name": "enum_customers_industry", + "schema": "public", + "values": [ + "healthcare", + "fintech", + "ecommerce", + "education", + "gaming", + "media", + "real-estate", + "saas", + "social", + "analytics", + "ai", + "developer-tools" + ] + }, + "public.enum_customers_supabase_products": { + "name": "enum_customers_supabase_products", + "schema": "public", + "values": [ + "database", + "auth", + "storage", + "realtime", + "functions", + "vector" + ] + }, + "public.enum_customers_company_size": { + "name": "enum_customers_company_size", + "schema": "public", + "values": [ + "startup", + "enterprise", + "indie_dev" + ] + }, + "public.enum_customers_region": { + "name": "enum_customers_region", + "schema": "public", + "values": [ + "Asia", + "Europe", + "North America", + "South America", + "Africa", + "Oceania" + ] + }, + "public.enum_customers_status": { + "name": "enum_customers_status", + "schema": "public", + "values": [ + "draft", + "published" + ] + }, + "public.enum__customers_v_version_industry": { + "name": "enum__customers_v_version_industry", + "schema": "public", + "values": [ + "healthcare", + "fintech", + "ecommerce", + "education", + "gaming", + "media", + "real-estate", + "saas", + "social", + "analytics", + "ai", + "developer-tools" + ] + }, + "public.enum__customers_v_version_supabase_products": { + "name": "enum__customers_v_version_supabase_products", + "schema": "public", + "values": [ + "database", + "auth", + "storage", + "realtime", + "functions", + "vector" + ] + }, + "public.enum__customers_v_version_company_size": { + "name": "enum__customers_v_version_company_size", + "schema": "public", + "values": [ + "startup", + "enterprise", + "indie_dev" + ] + }, + "public.enum__customers_v_version_region": { + "name": "enum__customers_v_version_region", + "schema": "public", + "values": [ + "Asia", + "Europe", + "North America", + "South America", + "Africa", + "Oceania" + ] + }, + "public.enum__customers_v_version_status": { + "name": "enum__customers_v_version_status", + "schema": "public", + "values": [ + "draft", + "published" + ] + }, + "public.enum_events_type": { + "name": "enum_events_type", + "schema": "public", + "values": [ + "conference", + "hackathon", + "launch-week", + "meetup", + "office-hours", + "talk", + "webinar", + "workshop", + "other" + ] + }, + "public.enum_events_timezone": { + "name": "enum_events_timezone", + "schema": "public", + "values": [ + "Africa/Abidjan", + "Africa/Accra", + "Africa/Addis_Ababa", + "Africa/Algiers", + "Africa/Asmara", + "Africa/Bamako", + "Africa/Bangui", + "Africa/Banjul", + "Africa/Bissau", + "Africa/Blantyre", + "Africa/Brazzaville", + "Africa/Bujumbura", + "Africa/Cairo", + "Africa/Casablanca", + "Africa/Ceuta", + "Africa/Conakry", + "Africa/Dakar", + "Africa/Dar_es_Salaam", + "Africa/Djibouti", + "Africa/Douala", + "Africa/El_Aaiun", + "Africa/Freetown", + "Africa/Gaborone", + "Africa/Harare", + "Africa/Johannesburg", + "Africa/Juba", + "Africa/Kampala", + "Africa/Khartoum", + "Africa/Kigali", + "Africa/Kinshasa", + "Africa/Lagos", + "Africa/Libreville", + "Africa/Lome", + "Africa/Luanda", + "Africa/Lubumbashi", + "Africa/Lusaka", + "Africa/Malabo", + "Africa/Maputo", + "Africa/Maseru", + "Africa/Mbabane", + "Africa/Mogadishu", + "Africa/Monrovia", + "Africa/Nairobi", + "Africa/Ndjamena", + "Africa/Niamey", + "Africa/Nouakchott", + "Africa/Ouagadougou", + "Africa/Porto-Novo", + "Africa/Sao_Tome", + "Africa/Tripoli", + "Africa/Tunis", + "Africa/Windhoek", + "America/Adak", + "America/Anchorage", + "America/Anguilla", + "America/Antigua", + "America/Araguaina", + "America/Argentina/Buenos_Aires", + "America/Argentina/Catamarca", + "America/Argentina/Cordoba", + "America/Argentina/Jujuy", + "America/Argentina/La_Rioja", + "America/Argentina/Mendoza", + "America/Argentina/Rio_Gallegos", + "America/Argentina/Salta", + "America/Argentina/San_Juan", + "America/Argentina/San_Luis", + "America/Argentina/Tucuman", + "America/Argentina/Ushuaia", + "America/Aruba", + "America/Asuncion", + "America/Atikokan", + "America/Bahia", + "America/Bahia_Banderas", + "America/Barbados", + "America/Belem", + "America/Belize", + "America/Blanc-Sablon", + "America/Boa_Vista", + "America/Bogota", + "America/Boise", + "America/Cambridge_Bay", + "America/Campo_Grande", + "America/Cancun", + "America/Caracas", + "America/Cayenne", + "America/Cayman", + "America/Chicago", + "America/Chihuahua", + "America/Costa_Rica", + "America/Creston", + "America/Cuiaba", + "America/Curacao", + "America/Danmarkshavn", + "America/Dawson", + "America/Dawson_Creek", + "America/Denver", + "America/Detroit", + "America/Dominica", + "America/Edmonton", + "America/Eirunepe", + "America/El_Salvador", + "America/Fort_Nelson", + "America/Fortaleza", + "America/Glace_Bay", + "America/Godthab", + "America/Goose_Bay", + "America/Grand_Turk", + "America/Grenada", + "America/Guadeloupe", + "America/Guatemala", + "America/Guayaquil", + "America/Guyana", + "America/Halifax", + "America/Havana", + "America/Hermosillo", + "America/Indiana/Indianapolis", + "America/Indiana/Knox", + "America/Indiana/Marengo", + "America/Indiana/Petersburg", + "America/Indiana/Tell_City", + "America/Indiana/Vevay", + "America/Indiana/Vincennes", + "America/Indiana/Winamac", + "America/Inuvik", + "America/Iqaluit", + "America/Jamaica", + "America/Juneau", + "America/Kentucky/Louisville", + "America/Kentucky/Monticello", + "America/Kralendijk", + "America/La_Paz", + "America/Lima", + "America/Los_Angeles", + "America/Lower_Princes", + "America/Maceio", + "America/Managua", + "America/Manaus", + "America/Marigot", + "America/Martinique", + "America/Matamoros", + "America/Mazatlan", + "America/Menominee", + "America/Merida", + "America/Metlakatla", + "America/Mexico_City", + "America/Miquelon", + "America/Moncton", + "America/Monterrey", + "America/Montevideo", + "America/Montserrat", + "America/Nassau", + "America/New_York", + "America/Nipigon", + "America/Nome", + "America/Noronha", + "America/North_Dakota/Beulah", + "America/North_Dakota/Center", + "America/North_Dakota/New_Salem", + "America/Ojinaga", + "America/Panama", + "America/Pangnirtung", + "America/Paramaribo", + "America/Phoenix", + "America/Port-au-Prince", + "America/Port_of_Spain", + "America/Porto_Velho", + "America/Puerto_Rico", + "America/Punta_Arenas", + "America/Rainy_River", + "America/Rankin_Inlet", + "America/Recife", + "America/Regina", + "America/Resolute", + "America/Rio_Branco", + "America/Santarem", + "America/Santiago", + "America/Santo_Domingo", + "America/Sao_Paulo", + "America/Scoresbysund", + "America/Sitka", + "America/St_Barthelemy", + "America/St_Johns", + "America/St_Kitts", + "America/St_Lucia", + "America/St_Thomas", + "America/St_Vincent", + "America/Swift_Current", + "America/Tegucigalpa", + "America/Thule", + "America/Thunder_Bay", + "America/Tijuana", + "America/Toronto", + "America/Tortola", + "America/Vancouver", + "America/Whitehorse", + "America/Winnipeg", + "America/Yakutat", + "America/Yellowknife", + "Antarctica/Casey", + "Antarctica/Davis", + "Antarctica/DumontDUrville", + "Antarctica/Macquarie", + "Antarctica/Mawson", + "Antarctica/McMurdo", + "Antarctica/Palmer", + "Antarctica/Rothera", + "Antarctica/Syowa", + "Antarctica/Troll", + "Antarctica/Vostok", + "Arctic/Longyearbyen", + "Asia/Aden", + "Asia/Almaty", + "Asia/Amman", + "Asia/Anadyr", + "Asia/Aqtau", + "Asia/Aqtobe", + "Asia/Ashgabat", + "Asia/Atyrau", + "Asia/Baghdad", + "Asia/Bahrain", + "Asia/Baku", + "Asia/Bangkok", + "Asia/Barnaul", + "Asia/Beirut", + "Asia/Bishkek", + "Asia/Brunei", + "Asia/Chita", + "Asia/Choibalsan", + "Asia/Colombo", + "Asia/Damascus", + "Asia/Dhaka", + "Asia/Dili", + "Asia/Dubai", + "Asia/Dushanbe", + "Asia/Famagusta", + "Asia/Gaza", + "Asia/Hebron", + "Asia/Ho_Chi_Minh", + "Asia/Hong_Kong", + "Asia/Hovd", + "Asia/Irkutsk", + "Asia/Istanbul", + "Asia/Jakarta", + "Asia/Jayapura", + "Asia/Jerusalem", + "Asia/Kabul", + "Asia/Kamchatka", + "Asia/Karachi", + "Asia/Kathmandu", + "Asia/Khandyga", + "Asia/Kolkata", + "Asia/Krasnoyarsk", + "Asia/Kuala_Lumpur", + "Asia/Kuching", + "Asia/Kuwait", + "Asia/Macau", + "Asia/Magadan", + "Asia/Makassar", + "Asia/Manila", + "Asia/Muscat", + "Asia/Nicosia", + "Asia/Novokuznetsk", + "Asia/Novosibirsk", + "Asia/Omsk", + "Asia/Oral", + "Asia/Phnom_Penh", + "Asia/Pontianak", + "Asia/Pyongyang", + "Asia/Qatar", + "Asia/Qostanay", + "Asia/Qyzylorda", + "Asia/Riyadh", + "Asia/Sakhalin", + "Asia/Samarkand", + "Asia/Seoul", + "Asia/Shanghai", + "Asia/Singapore", + "Asia/Srednekolymsk", + "Asia/Taipei", + "Asia/Tashkent", + "Asia/Tbilisi", + "Asia/Tehran", + "Asia/Thimphu", + "Asia/Tokyo", + "Asia/Tomsk", + "Asia/Ulaanbaatar", + "Asia/Urumqi", + "Asia/Ust-Nera", + "Asia/Vientiane", + "Asia/Vladivostok", + "Asia/Yakutsk", + "Asia/Yangon", + "Asia/Yekaterinburg", + "Asia/Yerevan", + "Atlantic/Azores", + "Atlantic/Bermuda", + "Atlantic/Canary", + "Atlantic/Cape_Verde", + "Atlantic/Faroe", + "Atlantic/Madeira", + "Atlantic/Reykjavik", + "Atlantic/South_Georgia", + "Atlantic/St_Helena", + "Atlantic/Stanley", + "Australia/Adelaide", + "Australia/Brisbane", + "Australia/Broken_Hill", + "Australia/Currie", + "Australia/Darwin", + "Australia/Eucla", + "Australia/Hobart", + "Australia/Lindeman", + "Australia/Lord_Howe", + "Australia/Melbourne", + "Australia/Perth", + "Australia/Sydney", + "Europe/Amsterdam", + "Europe/Andorra", + "Europe/Astrakhan", + "Europe/Athens", + "Europe/Belgrade", + "Europe/Berlin", + "Europe/Bratislava", + "Europe/Brussels", + "Europe/Bucharest", + "Europe/Budapest", + "Europe/Busingen", + "Europe/Chisinau", + "Europe/Copenhagen", + "Europe/Dublin", + "Europe/Gibraltar", + "Europe/Guernsey", + "Europe/Helsinki", + "Europe/Isle_of_Man", + "Europe/Istanbul", + "Europe/Jersey", + "Europe/Kaliningrad", + "Europe/Kiev", + "Europe/Kirov", + "Europe/Lisbon", + "Europe/Ljubljana", + "Europe/London", + "Europe/Luxembourg", + "Europe/Madrid", + "Europe/Malta", + "Europe/Mariehamn", + "Europe/Minsk", + "Europe/Monaco", + "Europe/Moscow", + "Europe/Oslo", + "Europe/Paris", + "Europe/Podgorica", + "Europe/Prague", + "Europe/Riga", + "Europe/Rome", + "Europe/Samara", + "Europe/San_Marino", + "Europe/Sarajevo", + "Europe/Saratov", + "Europe/Simferopol", + "Europe/Skopje", + "Europe/Sofia", + "Europe/Stockholm", + "Europe/Tallinn", + "Europe/Tirane", + "Europe/Ulyanovsk", + "Europe/Uzhgorod", + "Europe/Vaduz", + "Europe/Vatican", + "Europe/Vienna", + "Europe/Vilnius", + "Europe/Volgograd", + "Europe/Warsaw", + "Europe/Zagreb", + "Europe/Zaporozhye", + "Europe/Zurich", + "Indian/Antananarivo", + "Indian/Chagos", + "Indian/Christmas", + "Indian/Cocos", + "Indian/Comoro", + "Indian/Kerguelen", + "Indian/Mahe", + "Indian/Maldives", + "Indian/Mauritius", + "Indian/Mayotte", + "Indian/Reunion", + "Pacific/Apia", + "Pacific/Auckland", + "Pacific/Bougainville", + "Pacific/Chatham", + "Pacific/Chuuk", + "Pacific/Easter", + "Pacific/Efate", + "Pacific/Enderbury", + "Pacific/Fakaofo", + "Pacific/Fiji", + "Pacific/Funafuti", + "Pacific/Galapagos", + "Pacific/Gambier", + "Pacific/Guadalcanal", + "Pacific/Guam", + "Pacific/Honolulu", + "Pacific/Kiritimati", + "Pacific/Kosrae", + "Pacific/Kwajalein", + "Pacific/Majuro", + "Pacific/Marquesas", + "Pacific/Midway", + "Pacific/Nauru", + "Pacific/Niue", + "Pacific/Norfolk", + "Pacific/Noumea", + "Pacific/Pago_Pago", + "Pacific/Palau", + "Pacific/Pitcairn", + "Pacific/Pohnpei", + "Pacific/Port_Moresby", + "Pacific/Rarotonga", + "Pacific/Saipan", + "Pacific/Tahiti", + "Pacific/Tarawa", + "Pacific/Tongatapu", + "Pacific/Wake", + "Pacific/Wallis", + "UTC" + ] + }, + "public.enum_events_link_target": { + "name": "enum_events_link_target", + "schema": "public", + "values": [ + "_self", + "_blank" + ] + }, + "public.enum_events_main_cta_target": { + "name": "enum_events_main_cta_target", + "schema": "public", + "values": [ + "_self", + "_blank" + ] + }, + "public.enum_events_status": { + "name": "enum_events_status", + "schema": "public", + "values": [ + "draft", + "published" + ] + }, + "public.enum__events_v_version_type": { + "name": "enum__events_v_version_type", + "schema": "public", + "values": [ + "conference", + "hackathon", + "launch-week", + "meetup", + "office-hours", + "talk", + "webinar", + "workshop", + "other" + ] + }, + "public.enum__events_v_version_timezone": { + "name": "enum__events_v_version_timezone", + "schema": "public", + "values": [ + "Africa/Abidjan", + "Africa/Accra", + "Africa/Addis_Ababa", + "Africa/Algiers", + "Africa/Asmara", + "Africa/Bamako", + "Africa/Bangui", + "Africa/Banjul", + "Africa/Bissau", + "Africa/Blantyre", + "Africa/Brazzaville", + "Africa/Bujumbura", + "Africa/Cairo", + "Africa/Casablanca", + "Africa/Ceuta", + "Africa/Conakry", + "Africa/Dakar", + "Africa/Dar_es_Salaam", + "Africa/Djibouti", + "Africa/Douala", + "Africa/El_Aaiun", + "Africa/Freetown", + "Africa/Gaborone", + "Africa/Harare", + "Africa/Johannesburg", + "Africa/Juba", + "Africa/Kampala", + "Africa/Khartoum", + "Africa/Kigali", + "Africa/Kinshasa", + "Africa/Lagos", + "Africa/Libreville", + "Africa/Lome", + "Africa/Luanda", + "Africa/Lubumbashi", + "Africa/Lusaka", + "Africa/Malabo", + "Africa/Maputo", + "Africa/Maseru", + "Africa/Mbabane", + "Africa/Mogadishu", + "Africa/Monrovia", + "Africa/Nairobi", + "Africa/Ndjamena", + "Africa/Niamey", + "Africa/Nouakchott", + "Africa/Ouagadougou", + "Africa/Porto-Novo", + "Africa/Sao_Tome", + "Africa/Tripoli", + "Africa/Tunis", + "Africa/Windhoek", + "America/Adak", + "America/Anchorage", + "America/Anguilla", + "America/Antigua", + "America/Araguaina", + "America/Argentina/Buenos_Aires", + "America/Argentina/Catamarca", + "America/Argentina/Cordoba", + "America/Argentina/Jujuy", + "America/Argentina/La_Rioja", + "America/Argentina/Mendoza", + "America/Argentina/Rio_Gallegos", + "America/Argentina/Salta", + "America/Argentina/San_Juan", + "America/Argentina/San_Luis", + "America/Argentina/Tucuman", + "America/Argentina/Ushuaia", + "America/Aruba", + "America/Asuncion", + "America/Atikokan", + "America/Bahia", + "America/Bahia_Banderas", + "America/Barbados", + "America/Belem", + "America/Belize", + "America/Blanc-Sablon", + "America/Boa_Vista", + "America/Bogota", + "America/Boise", + "America/Cambridge_Bay", + "America/Campo_Grande", + "America/Cancun", + "America/Caracas", + "America/Cayenne", + "America/Cayman", + "America/Chicago", + "America/Chihuahua", + "America/Costa_Rica", + "America/Creston", + "America/Cuiaba", + "America/Curacao", + "America/Danmarkshavn", + "America/Dawson", + "America/Dawson_Creek", + "America/Denver", + "America/Detroit", + "America/Dominica", + "America/Edmonton", + "America/Eirunepe", + "America/El_Salvador", + "America/Fort_Nelson", + "America/Fortaleza", + "America/Glace_Bay", + "America/Godthab", + "America/Goose_Bay", + "America/Grand_Turk", + "America/Grenada", + "America/Guadeloupe", + "America/Guatemala", + "America/Guayaquil", + "America/Guyana", + "America/Halifax", + "America/Havana", + "America/Hermosillo", + "America/Indiana/Indianapolis", + "America/Indiana/Knox", + "America/Indiana/Marengo", + "America/Indiana/Petersburg", + "America/Indiana/Tell_City", + "America/Indiana/Vevay", + "America/Indiana/Vincennes", + "America/Indiana/Winamac", + "America/Inuvik", + "America/Iqaluit", + "America/Jamaica", + "America/Juneau", + "America/Kentucky/Louisville", + "America/Kentucky/Monticello", + "America/Kralendijk", + "America/La_Paz", + "America/Lima", + "America/Los_Angeles", + "America/Lower_Princes", + "America/Maceio", + "America/Managua", + "America/Manaus", + "America/Marigot", + "America/Martinique", + "America/Matamoros", + "America/Mazatlan", + "America/Menominee", + "America/Merida", + "America/Metlakatla", + "America/Mexico_City", + "America/Miquelon", + "America/Moncton", + "America/Monterrey", + "America/Montevideo", + "America/Montserrat", + "America/Nassau", + "America/New_York", + "America/Nipigon", + "America/Nome", + "America/Noronha", + "America/North_Dakota/Beulah", + "America/North_Dakota/Center", + "America/North_Dakota/New_Salem", + "America/Ojinaga", + "America/Panama", + "America/Pangnirtung", + "America/Paramaribo", + "America/Phoenix", + "America/Port-au-Prince", + "America/Port_of_Spain", + "America/Porto_Velho", + "America/Puerto_Rico", + "America/Punta_Arenas", + "America/Rainy_River", + "America/Rankin_Inlet", + "America/Recife", + "America/Regina", + "America/Resolute", + "America/Rio_Branco", + "America/Santarem", + "America/Santiago", + "America/Santo_Domingo", + "America/Sao_Paulo", + "America/Scoresbysund", + "America/Sitka", + "America/St_Barthelemy", + "America/St_Johns", + "America/St_Kitts", + "America/St_Lucia", + "America/St_Thomas", + "America/St_Vincent", + "America/Swift_Current", + "America/Tegucigalpa", + "America/Thule", + "America/Thunder_Bay", + "America/Tijuana", + "America/Toronto", + "America/Tortola", + "America/Vancouver", + "America/Whitehorse", + "America/Winnipeg", + "America/Yakutat", + "America/Yellowknife", + "Antarctica/Casey", + "Antarctica/Davis", + "Antarctica/DumontDUrville", + "Antarctica/Macquarie", + "Antarctica/Mawson", + "Antarctica/McMurdo", + "Antarctica/Palmer", + "Antarctica/Rothera", + "Antarctica/Syowa", + "Antarctica/Troll", + "Antarctica/Vostok", + "Arctic/Longyearbyen", + "Asia/Aden", + "Asia/Almaty", + "Asia/Amman", + "Asia/Anadyr", + "Asia/Aqtau", + "Asia/Aqtobe", + "Asia/Ashgabat", + "Asia/Atyrau", + "Asia/Baghdad", + "Asia/Bahrain", + "Asia/Baku", + "Asia/Bangkok", + "Asia/Barnaul", + "Asia/Beirut", + "Asia/Bishkek", + "Asia/Brunei", + "Asia/Chita", + "Asia/Choibalsan", + "Asia/Colombo", + "Asia/Damascus", + "Asia/Dhaka", + "Asia/Dili", + "Asia/Dubai", + "Asia/Dushanbe", + "Asia/Famagusta", + "Asia/Gaza", + "Asia/Hebron", + "Asia/Ho_Chi_Minh", + "Asia/Hong_Kong", + "Asia/Hovd", + "Asia/Irkutsk", + "Asia/Istanbul", + "Asia/Jakarta", + "Asia/Jayapura", + "Asia/Jerusalem", + "Asia/Kabul", + "Asia/Kamchatka", + "Asia/Karachi", + "Asia/Kathmandu", + "Asia/Khandyga", + "Asia/Kolkata", + "Asia/Krasnoyarsk", + "Asia/Kuala_Lumpur", + "Asia/Kuching", + "Asia/Kuwait", + "Asia/Macau", + "Asia/Magadan", + "Asia/Makassar", + "Asia/Manila", + "Asia/Muscat", + "Asia/Nicosia", + "Asia/Novokuznetsk", + "Asia/Novosibirsk", + "Asia/Omsk", + "Asia/Oral", + "Asia/Phnom_Penh", + "Asia/Pontianak", + "Asia/Pyongyang", + "Asia/Qatar", + "Asia/Qostanay", + "Asia/Qyzylorda", + "Asia/Riyadh", + "Asia/Sakhalin", + "Asia/Samarkand", + "Asia/Seoul", + "Asia/Shanghai", + "Asia/Singapore", + "Asia/Srednekolymsk", + "Asia/Taipei", + "Asia/Tashkent", + "Asia/Tbilisi", + "Asia/Tehran", + "Asia/Thimphu", + "Asia/Tokyo", + "Asia/Tomsk", + "Asia/Ulaanbaatar", + "Asia/Urumqi", + "Asia/Ust-Nera", + "Asia/Vientiane", + "Asia/Vladivostok", + "Asia/Yakutsk", + "Asia/Yangon", + "Asia/Yekaterinburg", + "Asia/Yerevan", + "Atlantic/Azores", + "Atlantic/Bermuda", + "Atlantic/Canary", + "Atlantic/Cape_Verde", + "Atlantic/Faroe", + "Atlantic/Madeira", + "Atlantic/Reykjavik", + "Atlantic/South_Georgia", + "Atlantic/St_Helena", + "Atlantic/Stanley", + "Australia/Adelaide", + "Australia/Brisbane", + "Australia/Broken_Hill", + "Australia/Currie", + "Australia/Darwin", + "Australia/Eucla", + "Australia/Hobart", + "Australia/Lindeman", + "Australia/Lord_Howe", + "Australia/Melbourne", + "Australia/Perth", + "Australia/Sydney", + "Europe/Amsterdam", + "Europe/Andorra", + "Europe/Astrakhan", + "Europe/Athens", + "Europe/Belgrade", + "Europe/Berlin", + "Europe/Bratislava", + "Europe/Brussels", + "Europe/Bucharest", + "Europe/Budapest", + "Europe/Busingen", + "Europe/Chisinau", + "Europe/Copenhagen", + "Europe/Dublin", + "Europe/Gibraltar", + "Europe/Guernsey", + "Europe/Helsinki", + "Europe/Isle_of_Man", + "Europe/Istanbul", + "Europe/Jersey", + "Europe/Kaliningrad", + "Europe/Kiev", + "Europe/Kirov", + "Europe/Lisbon", + "Europe/Ljubljana", + "Europe/London", + "Europe/Luxembourg", + "Europe/Madrid", + "Europe/Malta", + "Europe/Mariehamn", + "Europe/Minsk", + "Europe/Monaco", + "Europe/Moscow", + "Europe/Oslo", + "Europe/Paris", + "Europe/Podgorica", + "Europe/Prague", + "Europe/Riga", + "Europe/Rome", + "Europe/Samara", + "Europe/San_Marino", + "Europe/Sarajevo", + "Europe/Saratov", + "Europe/Simferopol", + "Europe/Skopje", + "Europe/Sofia", + "Europe/Stockholm", + "Europe/Tallinn", + "Europe/Tirane", + "Europe/Ulyanovsk", + "Europe/Uzhgorod", + "Europe/Vaduz", + "Europe/Vatican", + "Europe/Vienna", + "Europe/Vilnius", + "Europe/Volgograd", + "Europe/Warsaw", + "Europe/Zagreb", + "Europe/Zaporozhye", + "Europe/Zurich", + "Indian/Antananarivo", + "Indian/Chagos", + "Indian/Christmas", + "Indian/Cocos", + "Indian/Comoro", + "Indian/Kerguelen", + "Indian/Mahe", + "Indian/Maldives", + "Indian/Mauritius", + "Indian/Mayotte", + "Indian/Reunion", + "Pacific/Apia", + "Pacific/Auckland", + "Pacific/Bougainville", + "Pacific/Chatham", + "Pacific/Chuuk", + "Pacific/Easter", + "Pacific/Efate", + "Pacific/Enderbury", + "Pacific/Fakaofo", + "Pacific/Fiji", + "Pacific/Funafuti", + "Pacific/Galapagos", + "Pacific/Gambier", + "Pacific/Guadalcanal", + "Pacific/Guam", + "Pacific/Honolulu", + "Pacific/Kiritimati", + "Pacific/Kosrae", + "Pacific/Kwajalein", + "Pacific/Majuro", + "Pacific/Marquesas", + "Pacific/Midway", + "Pacific/Nauru", + "Pacific/Niue", + "Pacific/Norfolk", + "Pacific/Noumea", + "Pacific/Pago_Pago", + "Pacific/Palau", + "Pacific/Pitcairn", + "Pacific/Pohnpei", + "Pacific/Port_Moresby", + "Pacific/Rarotonga", + "Pacific/Saipan", + "Pacific/Tahiti", + "Pacific/Tarawa", + "Pacific/Tongatapu", + "Pacific/Wake", + "Pacific/Wallis", + "UTC" + ] + }, + "public.enum__events_v_version_link_target": { + "name": "enum__events_v_version_link_target", + "schema": "public", + "values": [ + "_self", + "_blank" + ] + }, + "public.enum__events_v_version_main_cta_target": { + "name": "enum__events_v_version_main_cta_target", + "schema": "public", + "values": [ + "_self", + "_blank" + ] + }, + "public.enum__events_v_version_status": { + "name": "enum__events_v_version_status", + "schema": "public", + "values": [ + "draft", + "published" + ] + }, + "public.enum_posts_launchweek": { + "name": "enum_posts_launchweek", + "schema": "public", + "values": [ + "6", + "7", + "8", + "x", + "ga", + "12", + "13", + "14", + "15" + ] + }, + "public.enum_posts_status": { + "name": "enum_posts_status", + "schema": "public", + "values": [ + "draft", + "published" + ] + }, + "public.enum__posts_v_version_launchweek": { + "name": "enum__posts_v_version_launchweek", + "schema": "public", + "values": [ + "6", + "7", + "8", + "x", + "ga", + "12", + "13", + "14", + "15" + ] + }, + "public.enum__posts_v_version_status": { + "name": "enum__posts_v_version_status", + "schema": "public", + "values": [ + "draft", + "published" + ] + }, + "public.enum_users_roles": { + "name": "enum_users_roles", + "schema": "public", + "values": [ + "admin", + "editor" + ] + }, + "public.enum_payload_jobs_log_task_slug": { + "name": "enum_payload_jobs_log_task_slug", + "schema": "public", + "values": [ + "inline", + "schedulePublish" + ] + }, + "public.enum_payload_jobs_log_state": { + "name": "enum_payload_jobs_log_state", + "schema": "public", + "values": [ + "failed", + "succeeded" + ] + }, + "public.enum_payload_jobs_task_slug": { + "name": "enum_payload_jobs_task_slug", + "schema": "public", + "values": [ + "inline", + "schedulePublish" + ] + } + }, + "schemas": {}, + "sequences": {}, + "roles": {}, + "policies": {}, + "views": {}, + "_meta": { + "schemas": {}, + "tables": {}, + "columns": {} + } +} \ No newline at end of file diff --git a/apps/cms/src/migrations/20250619_160549.ts b/apps/cms/src/migrations/20250619_160549.ts new file mode 100644 index 00000000000..2e1512fd771 --- /dev/null +++ b/apps/cms/src/migrations/20250619_160549.ts @@ -0,0 +1,15 @@ +import { MigrateUpArgs, MigrateDownArgs, sql } from '@payloadcms/db-postgres' + +export async function up({ db }: MigrateUpArgs): Promise { + await db.execute(sql` + INSERT INTO storage.buckets (id, name, public) VALUES ('cms', 'cms', false) + ON CONFLICT (id) DO NOTHING; + `) +} + +export async function down({ db }: MigrateDownArgs): Promise { + await db.execute(sql` + DELETE FROM storage.objects WHERE bucket_id = 'cms'; + DELETE FROM storage.buckets WHERE id = 'cms'; + `) +} \ No newline at end of file diff --git a/apps/cms/src/migrations/20250808_145722_update_payload_migrations.json b/apps/cms/src/migrations/20250808_145722_update_payload_migrations.json new file mode 100644 index 00000000000..cc014d97c68 --- /dev/null +++ b/apps/cms/src/migrations/20250808_145722_update_payload_migrations.json @@ -0,0 +1,7418 @@ +{ + "id": "a2444573-aed4-499a-8737-fffaf84caa24", + "prevId": "00000000-0000-0000-0000-000000000000", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.authors": { + "name": "authors", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "author": { + "name": "author", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "author_id": { + "name": "author_id", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "position": { + "name": "position", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "author_url": { + "name": "author_url", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "author_image_url_id": { + "name": "author_image_url_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "username": { + "name": "username", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "created_at": { + "name": "created_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "authors_author_image_url_idx": { + "name": "authors_author_image_url_idx", + "columns": [ + { + "expression": "author_image_url_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "authors_updated_at_idx": { + "name": "authors_updated_at_idx", + "columns": [ + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "authors_created_at_idx": { + "name": "authors_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "authors_author_image_url_id_media_id_fk": { + "name": "authors_author_image_url_id_media_id_fk", + "tableFrom": "authors", + "tableTo": "media", + "columnsFrom": [ + "author_image_url_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.categories_breadcrumbs": { + "name": "categories_breadcrumbs", + "schema": "", + "columns": { + "_order": { + "name": "_order", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "_parent_id": { + "name": "_parent_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "id": { + "name": "id", + "type": "varchar", + "primaryKey": true, + "notNull": true + }, + "doc_id": { + "name": "doc_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "url": { + "name": "url", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "label": { + "name": "label", + "type": "varchar", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "categories_breadcrumbs_order_idx": { + "name": "categories_breadcrumbs_order_idx", + "columns": [ + { + "expression": "_order", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "categories_breadcrumbs_parent_id_idx": { + "name": "categories_breadcrumbs_parent_id_idx", + "columns": [ + { + "expression": "_parent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "categories_breadcrumbs_doc_idx": { + "name": "categories_breadcrumbs_doc_idx", + "columns": [ + { + "expression": "doc_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "categories_breadcrumbs_doc_id_categories_id_fk": { + "name": "categories_breadcrumbs_doc_id_categories_id_fk", + "tableFrom": "categories_breadcrumbs", + "tableTo": "categories", + "columnsFrom": [ + "doc_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "categories_breadcrumbs_parent_id_fk": { + "name": "categories_breadcrumbs_parent_id_fk", + "tableFrom": "categories_breadcrumbs", + "tableTo": "categories", + "columnsFrom": [ + "_parent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.categories": { + "name": "categories", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "parent_id": { + "name": "parent_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "created_at": { + "name": "created_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "categories_parent_idx": { + "name": "categories_parent_idx", + "columns": [ + { + "expression": "parent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "categories_updated_at_idx": { + "name": "categories_updated_at_idx", + "columns": [ + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "categories_created_at_idx": { + "name": "categories_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "categories_parent_id_categories_id_fk": { + "name": "categories_parent_id_categories_id_fk", + "tableFrom": "categories", + "tableTo": "categories", + "columnsFrom": [ + "parent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.customers_stats": { + "name": "customers_stats", + "schema": "", + "columns": { + "_order": { + "name": "_order", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "_parent_id": { + "name": "_parent_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "id": { + "name": "id", + "type": "varchar", + "primaryKey": true, + "notNull": true + }, + "stat": { + "name": "stat", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "label": { + "name": "label", + "type": "varchar", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "customers_stats_order_idx": { + "name": "customers_stats_order_idx", + "columns": [ + { + "expression": "_order", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "customers_stats_parent_id_idx": { + "name": "customers_stats_parent_id_idx", + "columns": [ + { + "expression": "_parent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "customers_stats_parent_id_fk": { + "name": "customers_stats_parent_id_fk", + "tableFrom": "customers_stats", + "tableTo": "customers", + "columnsFrom": [ + "_parent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.customers_misc": { + "name": "customers_misc", + "schema": "", + "columns": { + "_order": { + "name": "_order", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "_parent_id": { + "name": "_parent_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "id": { + "name": "id", + "type": "varchar", + "primaryKey": true, + "notNull": true + }, + "label": { + "name": "label", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "text": { + "name": "text", + "type": "varchar", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "customers_misc_order_idx": { + "name": "customers_misc_order_idx", + "columns": [ + { + "expression": "_order", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "customers_misc_parent_id_idx": { + "name": "customers_misc_parent_id_idx", + "columns": [ + { + "expression": "_parent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "customers_misc_parent_id_fk": { + "name": "customers_misc_parent_id_fk", + "tableFrom": "customers_misc", + "tableTo": "customers", + "columnsFrom": [ + "_parent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.customers_industry": { + "name": "customers_industry", + "schema": "", + "columns": { + "order": { + "name": "order", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "parent_id": { + "name": "parent_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "value": { + "name": "value", + "type": "enum_customers_industry", + "typeSchema": "public", + "primaryKey": false, + "notNull": false + }, + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + } + }, + "indexes": { + "customers_industry_order_idx": { + "name": "customers_industry_order_idx", + "columns": [ + { + "expression": "order", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "customers_industry_parent_idx": { + "name": "customers_industry_parent_idx", + "columns": [ + { + "expression": "parent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "customers_industry_parent_fk": { + "name": "customers_industry_parent_fk", + "tableFrom": "customers_industry", + "tableTo": "customers", + "columnsFrom": [ + "parent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.customers_supabase_products": { + "name": "customers_supabase_products", + "schema": "", + "columns": { + "order": { + "name": "order", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "parent_id": { + "name": "parent_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "value": { + "name": "value", + "type": "enum_customers_supabase_products", + "typeSchema": "public", + "primaryKey": false, + "notNull": false + }, + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + } + }, + "indexes": { + "customers_supabase_products_order_idx": { + "name": "customers_supabase_products_order_idx", + "columns": [ + { + "expression": "order", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "customers_supabase_products_parent_idx": { + "name": "customers_supabase_products_parent_idx", + "columns": [ + { + "expression": "parent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "customers_supabase_products_parent_fk": { + "name": "customers_supabase_products_parent_fk", + "tableFrom": "customers_supabase_products", + "tableTo": "customers", + "columnsFrom": [ + "parent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.customers": { + "name": "customers", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "title": { + "name": "title", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "slug": { + "name": "slug", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "slug_lock": { + "name": "slug_lock", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": true + }, + "content": { + "name": "content", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "description": { + "name": "description", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "about": { + "name": "about", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "company_url": { + "name": "company_url", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "company_size": { + "name": "company_size", + "type": "enum_customers_company_size", + "typeSchema": "public", + "primaryKey": false, + "notNull": false + }, + "region": { + "name": "region", + "type": "enum_customers_region", + "typeSchema": "public", + "primaryKey": false, + "notNull": false + }, + "meta_title": { + "name": "meta_title", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "meta_image_id": { + "name": "meta_image_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "meta_description": { + "name": "meta_description", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "published_at": { + "name": "published_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": false + }, + "logo_id": { + "name": "logo_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "logo_inverse_id": { + "name": "logo_inverse_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "created_at": { + "name": "created_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "_status": { + "name": "_status", + "type": "enum_customers_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": false, + "default": "'draft'" + } + }, + "indexes": { + "customers_slug_idx": { + "name": "customers_slug_idx", + "columns": [ + { + "expression": "slug", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "customers_meta_meta_image_idx": { + "name": "customers_meta_meta_image_idx", + "columns": [ + { + "expression": "meta_image_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "customers_logo_idx": { + "name": "customers_logo_idx", + "columns": [ + { + "expression": "logo_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "customers_logo_inverse_idx": { + "name": "customers_logo_inverse_idx", + "columns": [ + { + "expression": "logo_inverse_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "customers_updated_at_idx": { + "name": "customers_updated_at_idx", + "columns": [ + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "customers_created_at_idx": { + "name": "customers_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "customers__status_idx": { + "name": "customers__status_idx", + "columns": [ + { + "expression": "_status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "customers_meta_image_id_media_id_fk": { + "name": "customers_meta_image_id_media_id_fk", + "tableFrom": "customers", + "tableTo": "media", + "columnsFrom": [ + "meta_image_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "customers_logo_id_media_id_fk": { + "name": "customers_logo_id_media_id_fk", + "tableFrom": "customers", + "tableTo": "media", + "columnsFrom": [ + "logo_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "customers_logo_inverse_id_media_id_fk": { + "name": "customers_logo_inverse_id_media_id_fk", + "tableFrom": "customers", + "tableTo": "media", + "columnsFrom": [ + "logo_inverse_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public._customers_v_version_stats": { + "name": "_customers_v_version_stats", + "schema": "", + "columns": { + "_order": { + "name": "_order", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "_parent_id": { + "name": "_parent_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "stat": { + "name": "stat", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "label": { + "name": "label", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "_uuid": { + "name": "_uuid", + "type": "varchar", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "_customers_v_version_stats_order_idx": { + "name": "_customers_v_version_stats_order_idx", + "columns": [ + { + "expression": "_order", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "_customers_v_version_stats_parent_id_idx": { + "name": "_customers_v_version_stats_parent_id_idx", + "columns": [ + { + "expression": "_parent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "_customers_v_version_stats_parent_id_fk": { + "name": "_customers_v_version_stats_parent_id_fk", + "tableFrom": "_customers_v_version_stats", + "tableTo": "_customers_v", + "columnsFrom": [ + "_parent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public._customers_v_version_misc": { + "name": "_customers_v_version_misc", + "schema": "", + "columns": { + "_order": { + "name": "_order", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "_parent_id": { + "name": "_parent_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "label": { + "name": "label", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "text": { + "name": "text", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "_uuid": { + "name": "_uuid", + "type": "varchar", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "_customers_v_version_misc_order_idx": { + "name": "_customers_v_version_misc_order_idx", + "columns": [ + { + "expression": "_order", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "_customers_v_version_misc_parent_id_idx": { + "name": "_customers_v_version_misc_parent_id_idx", + "columns": [ + { + "expression": "_parent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "_customers_v_version_misc_parent_id_fk": { + "name": "_customers_v_version_misc_parent_id_fk", + "tableFrom": "_customers_v_version_misc", + "tableTo": "_customers_v", + "columnsFrom": [ + "_parent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public._customers_v_version_industry": { + "name": "_customers_v_version_industry", + "schema": "", + "columns": { + "order": { + "name": "order", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "parent_id": { + "name": "parent_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "value": { + "name": "value", + "type": "enum__customers_v_version_industry", + "typeSchema": "public", + "primaryKey": false, + "notNull": false + }, + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + } + }, + "indexes": { + "_customers_v_version_industry_order_idx": { + "name": "_customers_v_version_industry_order_idx", + "columns": [ + { + "expression": "order", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "_customers_v_version_industry_parent_idx": { + "name": "_customers_v_version_industry_parent_idx", + "columns": [ + { + "expression": "parent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "_customers_v_version_industry_parent_fk": { + "name": "_customers_v_version_industry_parent_fk", + "tableFrom": "_customers_v_version_industry", + "tableTo": "_customers_v", + "columnsFrom": [ + "parent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public._customers_v_version_supabase_products": { + "name": "_customers_v_version_supabase_products", + "schema": "", + "columns": { + "order": { + "name": "order", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "parent_id": { + "name": "parent_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "value": { + "name": "value", + "type": "enum__customers_v_version_supabase_products", + "typeSchema": "public", + "primaryKey": false, + "notNull": false + }, + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + } + }, + "indexes": { + "_customers_v_version_supabase_products_order_idx": { + "name": "_customers_v_version_supabase_products_order_idx", + "columns": [ + { + "expression": "order", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "_customers_v_version_supabase_products_parent_idx": { + "name": "_customers_v_version_supabase_products_parent_idx", + "columns": [ + { + "expression": "parent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "_customers_v_version_supabase_products_parent_fk": { + "name": "_customers_v_version_supabase_products_parent_fk", + "tableFrom": "_customers_v_version_supabase_products", + "tableTo": "_customers_v", + "columnsFrom": [ + "parent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public._customers_v": { + "name": "_customers_v", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "parent_id": { + "name": "parent_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "version_name": { + "name": "version_name", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "version_title": { + "name": "version_title", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "version_slug": { + "name": "version_slug", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "version_slug_lock": { + "name": "version_slug_lock", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": true + }, + "version_content": { + "name": "version_content", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "version_description": { + "name": "version_description", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "version_about": { + "name": "version_about", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "version_company_url": { + "name": "version_company_url", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "version_company_size": { + "name": "version_company_size", + "type": "enum__customers_v_version_company_size", + "typeSchema": "public", + "primaryKey": false, + "notNull": false + }, + "version_region": { + "name": "version_region", + "type": "enum__customers_v_version_region", + "typeSchema": "public", + "primaryKey": false, + "notNull": false + }, + "version_meta_title": { + "name": "version_meta_title", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "version_meta_image_id": { + "name": "version_meta_image_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "version_meta_description": { + "name": "version_meta_description", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "version_published_at": { + "name": "version_published_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": false + }, + "version_logo_id": { + "name": "version_logo_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "version_logo_inverse_id": { + "name": "version_logo_inverse_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "version_updated_at": { + "name": "version_updated_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": false + }, + "version_created_at": { + "name": "version_created_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": false + }, + "version__status": { + "name": "version__status", + "type": "enum__customers_v_version_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": false, + "default": "'draft'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "latest": { + "name": "latest", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "autosave": { + "name": "autosave", + "type": "boolean", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "_customers_v_parent_idx": { + "name": "_customers_v_parent_idx", + "columns": [ + { + "expression": "parent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "_customers_v_version_version_slug_idx": { + "name": "_customers_v_version_version_slug_idx", + "columns": [ + { + "expression": "version_slug", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "_customers_v_version_meta_version_meta_image_idx": { + "name": "_customers_v_version_meta_version_meta_image_idx", + "columns": [ + { + "expression": "version_meta_image_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "_customers_v_version_version_logo_idx": { + "name": "_customers_v_version_version_logo_idx", + "columns": [ + { + "expression": "version_logo_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "_customers_v_version_version_logo_inverse_idx": { + "name": "_customers_v_version_version_logo_inverse_idx", + "columns": [ + { + "expression": "version_logo_inverse_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "_customers_v_version_version_updated_at_idx": { + "name": "_customers_v_version_version_updated_at_idx", + "columns": [ + { + "expression": "version_updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "_customers_v_version_version_created_at_idx": { + "name": "_customers_v_version_version_created_at_idx", + "columns": [ + { + "expression": "version_created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "_customers_v_version_version__status_idx": { + "name": "_customers_v_version_version__status_idx", + "columns": [ + { + "expression": "version__status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "_customers_v_created_at_idx": { + "name": "_customers_v_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "_customers_v_updated_at_idx": { + "name": "_customers_v_updated_at_idx", + "columns": [ + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "_customers_v_latest_idx": { + "name": "_customers_v_latest_idx", + "columns": [ + { + "expression": "latest", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "_customers_v_autosave_idx": { + "name": "_customers_v_autosave_idx", + "columns": [ + { + "expression": "autosave", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "_customers_v_parent_id_customers_id_fk": { + "name": "_customers_v_parent_id_customers_id_fk", + "tableFrom": "_customers_v", + "tableTo": "customers", + "columnsFrom": [ + "parent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "_customers_v_version_meta_image_id_media_id_fk": { + "name": "_customers_v_version_meta_image_id_media_id_fk", + "tableFrom": "_customers_v", + "tableTo": "media", + "columnsFrom": [ + "version_meta_image_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "_customers_v_version_logo_id_media_id_fk": { + "name": "_customers_v_version_logo_id_media_id_fk", + "tableFrom": "_customers_v", + "tableTo": "media", + "columnsFrom": [ + "version_logo_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "_customers_v_version_logo_inverse_id_media_id_fk": { + "name": "_customers_v_version_logo_inverse_id_media_id_fk", + "tableFrom": "_customers_v", + "tableTo": "media", + "columnsFrom": [ + "version_logo_inverse_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.events_type": { + "name": "events_type", + "schema": "", + "columns": { + "order": { + "name": "order", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "parent_id": { + "name": "parent_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "value": { + "name": "value", + "type": "enum_events_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": false + }, + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + } + }, + "indexes": { + "events_type_order_idx": { + "name": "events_type_order_idx", + "columns": [ + { + "expression": "order", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "events_type_parent_idx": { + "name": "events_type_parent_idx", + "columns": [ + { + "expression": "parent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "events_type_parent_fk": { + "name": "events_type_parent_fk", + "tableFrom": "events_type", + "tableTo": "events", + "columnsFrom": [ + "parent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.events": { + "name": "events", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "title": { + "name": "title", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "slug": { + "name": "slug", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "slug_lock": { + "name": "slug_lock", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": true + }, + "subtitle": { + "name": "subtitle", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "content": { + "name": "content", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "thumb_id": { + "name": "thumb_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "image_id": { + "name": "image_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "date": { + "name": "date", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": false + }, + "timezone": { + "name": "timezone", + "type": "enum_events_timezone", + "typeSchema": "public", + "primaryKey": false, + "notNull": false + }, + "show_end_date": { + "name": "show_end_date", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "end_date": { + "name": "end_date", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": false + }, + "duration": { + "name": "duration", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "on_demand": { + "name": "on_demand", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "disable_page_build": { + "name": "disable_page_build", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "link_href": { + "name": "link_href", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "link_target": { + "name": "link_target", + "type": "enum_events_link_target", + "typeSchema": "public", + "primaryKey": false, + "notNull": false, + "default": "'_blank'" + }, + "main_cta_href": { + "name": "main_cta_href", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "main_cta_target": { + "name": "main_cta_target", + "type": "enum_events_main_cta_target", + "typeSchema": "public", + "primaryKey": false, + "notNull": false, + "default": "'_blank'" + }, + "main_cta_label": { + "name": "main_cta_label", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "main_cta_disabled": { + "name": "main_cta_disabled", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "main_cta_disabled_label": { + "name": "main_cta_disabled_label", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "company_show_company": { + "name": "company_show_company", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "company_name": { + "name": "company_name", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "company_website_url": { + "name": "company_website_url", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "company_logo_id": { + "name": "company_logo_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "company_logo_light_id": { + "name": "company_logo_light_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "participants_show_participants": { + "name": "participants_show_participants", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "meta_title": { + "name": "meta_title", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "meta_image_id": { + "name": "meta_image_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "meta_description": { + "name": "meta_description", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "published_at": { + "name": "published_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "created_at": { + "name": "created_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "_status": { + "name": "_status", + "type": "enum_events_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": false, + "default": "'draft'" + } + }, + "indexes": { + "events_slug_idx": { + "name": "events_slug_idx", + "columns": [ + { + "expression": "slug", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "events_thumb_idx": { + "name": "events_thumb_idx", + "columns": [ + { + "expression": "thumb_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "events_image_idx": { + "name": "events_image_idx", + "columns": [ + { + "expression": "image_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "events_company_company_logo_idx": { + "name": "events_company_company_logo_idx", + "columns": [ + { + "expression": "company_logo_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "events_company_company_logo_light_idx": { + "name": "events_company_company_logo_light_idx", + "columns": [ + { + "expression": "company_logo_light_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "events_meta_meta_image_idx": { + "name": "events_meta_meta_image_idx", + "columns": [ + { + "expression": "meta_image_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "events_updated_at_idx": { + "name": "events_updated_at_idx", + "columns": [ + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "events_created_at_idx": { + "name": "events_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "events__status_idx": { + "name": "events__status_idx", + "columns": [ + { + "expression": "_status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "events_thumb_id_media_id_fk": { + "name": "events_thumb_id_media_id_fk", + "tableFrom": "events", + "tableTo": "media", + "columnsFrom": [ + "thumb_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "events_image_id_media_id_fk": { + "name": "events_image_id_media_id_fk", + "tableFrom": "events", + "tableTo": "media", + "columnsFrom": [ + "image_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "events_company_logo_id_media_id_fk": { + "name": "events_company_logo_id_media_id_fk", + "tableFrom": "events", + "tableTo": "media", + "columnsFrom": [ + "company_logo_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "events_company_logo_light_id_media_id_fk": { + "name": "events_company_logo_light_id_media_id_fk", + "tableFrom": "events", + "tableTo": "media", + "columnsFrom": [ + "company_logo_light_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "events_meta_image_id_media_id_fk": { + "name": "events_meta_image_id_media_id_fk", + "tableFrom": "events", + "tableTo": "media", + "columnsFrom": [ + "meta_image_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.events_rels": { + "name": "events_rels", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "order": { + "name": "order", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "parent_id": { + "name": "parent_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "path": { + "name": "path", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "authors_id": { + "name": "authors_id", + "type": "integer", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "events_rels_order_idx": { + "name": "events_rels_order_idx", + "columns": [ + { + "expression": "order", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "events_rels_parent_idx": { + "name": "events_rels_parent_idx", + "columns": [ + { + "expression": "parent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "events_rels_path_idx": { + "name": "events_rels_path_idx", + "columns": [ + { + "expression": "path", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "events_rels_authors_id_idx": { + "name": "events_rels_authors_id_idx", + "columns": [ + { + "expression": "authors_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "events_rels_parent_fk": { + "name": "events_rels_parent_fk", + "tableFrom": "events_rels", + "tableTo": "events", + "columnsFrom": [ + "parent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "events_rels_authors_fk": { + "name": "events_rels_authors_fk", + "tableFrom": "events_rels", + "tableTo": "authors", + "columnsFrom": [ + "authors_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public._events_v_version_type": { + "name": "_events_v_version_type", + "schema": "", + "columns": { + "order": { + "name": "order", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "parent_id": { + "name": "parent_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "value": { + "name": "value", + "type": "enum__events_v_version_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": false + }, + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + } + }, + "indexes": { + "_events_v_version_type_order_idx": { + "name": "_events_v_version_type_order_idx", + "columns": [ + { + "expression": "order", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "_events_v_version_type_parent_idx": { + "name": "_events_v_version_type_parent_idx", + "columns": [ + { + "expression": "parent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "_events_v_version_type_parent_fk": { + "name": "_events_v_version_type_parent_fk", + "tableFrom": "_events_v_version_type", + "tableTo": "_events_v", + "columnsFrom": [ + "parent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public._events_v": { + "name": "_events_v", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "parent_id": { + "name": "parent_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "version_title": { + "name": "version_title", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "version_slug": { + "name": "version_slug", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "version_slug_lock": { + "name": "version_slug_lock", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": true + }, + "version_subtitle": { + "name": "version_subtitle", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "version_content": { + "name": "version_content", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "version_thumb_id": { + "name": "version_thumb_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "version_image_id": { + "name": "version_image_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "version_date": { + "name": "version_date", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": false + }, + "version_timezone": { + "name": "version_timezone", + "type": "enum__events_v_version_timezone", + "typeSchema": "public", + "primaryKey": false, + "notNull": false + }, + "version_show_end_date": { + "name": "version_show_end_date", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "version_end_date": { + "name": "version_end_date", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": false + }, + "version_duration": { + "name": "version_duration", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "version_on_demand": { + "name": "version_on_demand", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "version_disable_page_build": { + "name": "version_disable_page_build", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "version_link_href": { + "name": "version_link_href", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "version_link_target": { + "name": "version_link_target", + "type": "enum__events_v_version_link_target", + "typeSchema": "public", + "primaryKey": false, + "notNull": false, + "default": "'_blank'" + }, + "version_main_cta_href": { + "name": "version_main_cta_href", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "version_main_cta_target": { + "name": "version_main_cta_target", + "type": "enum__events_v_version_main_cta_target", + "typeSchema": "public", + "primaryKey": false, + "notNull": false, + "default": "'_blank'" + }, + "version_main_cta_label": { + "name": "version_main_cta_label", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "version_main_cta_disabled": { + "name": "version_main_cta_disabled", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "version_main_cta_disabled_label": { + "name": "version_main_cta_disabled_label", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "version_company_show_company": { + "name": "version_company_show_company", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "version_company_name": { + "name": "version_company_name", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "version_company_website_url": { + "name": "version_company_website_url", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "version_company_logo_id": { + "name": "version_company_logo_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "version_company_logo_light_id": { + "name": "version_company_logo_light_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "version_participants_show_participants": { + "name": "version_participants_show_participants", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "version_meta_title": { + "name": "version_meta_title", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "version_meta_image_id": { + "name": "version_meta_image_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "version_meta_description": { + "name": "version_meta_description", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "version_published_at": { + "name": "version_published_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": false + }, + "version_updated_at": { + "name": "version_updated_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": false + }, + "version_created_at": { + "name": "version_created_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": false + }, + "version__status": { + "name": "version__status", + "type": "enum__events_v_version_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": false, + "default": "'draft'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "latest": { + "name": "latest", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "autosave": { + "name": "autosave", + "type": "boolean", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "_events_v_parent_idx": { + "name": "_events_v_parent_idx", + "columns": [ + { + "expression": "parent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "_events_v_version_version_slug_idx": { + "name": "_events_v_version_version_slug_idx", + "columns": [ + { + "expression": "version_slug", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "_events_v_version_version_thumb_idx": { + "name": "_events_v_version_version_thumb_idx", + "columns": [ + { + "expression": "version_thumb_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "_events_v_version_version_image_idx": { + "name": "_events_v_version_version_image_idx", + "columns": [ + { + "expression": "version_image_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "_events_v_version_company_version_company_logo_idx": { + "name": "_events_v_version_company_version_company_logo_idx", + "columns": [ + { + "expression": "version_company_logo_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "_events_v_version_company_version_company_logo_light_idx": { + "name": "_events_v_version_company_version_company_logo_light_idx", + "columns": [ + { + "expression": "version_company_logo_light_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "_events_v_version_meta_version_meta_image_idx": { + "name": "_events_v_version_meta_version_meta_image_idx", + "columns": [ + { + "expression": "version_meta_image_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "_events_v_version_version_updated_at_idx": { + "name": "_events_v_version_version_updated_at_idx", + "columns": [ + { + "expression": "version_updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "_events_v_version_version_created_at_idx": { + "name": "_events_v_version_version_created_at_idx", + "columns": [ + { + "expression": "version_created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "_events_v_version_version__status_idx": { + "name": "_events_v_version_version__status_idx", + "columns": [ + { + "expression": "version__status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "_events_v_created_at_idx": { + "name": "_events_v_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "_events_v_updated_at_idx": { + "name": "_events_v_updated_at_idx", + "columns": [ + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "_events_v_latest_idx": { + "name": "_events_v_latest_idx", + "columns": [ + { + "expression": "latest", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "_events_v_autosave_idx": { + "name": "_events_v_autosave_idx", + "columns": [ + { + "expression": "autosave", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "_events_v_parent_id_events_id_fk": { + "name": "_events_v_parent_id_events_id_fk", + "tableFrom": "_events_v", + "tableTo": "events", + "columnsFrom": [ + "parent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "_events_v_version_thumb_id_media_id_fk": { + "name": "_events_v_version_thumb_id_media_id_fk", + "tableFrom": "_events_v", + "tableTo": "media", + "columnsFrom": [ + "version_thumb_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "_events_v_version_image_id_media_id_fk": { + "name": "_events_v_version_image_id_media_id_fk", + "tableFrom": "_events_v", + "tableTo": "media", + "columnsFrom": [ + "version_image_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "_events_v_version_company_logo_id_media_id_fk": { + "name": "_events_v_version_company_logo_id_media_id_fk", + "tableFrom": "_events_v", + "tableTo": "media", + "columnsFrom": [ + "version_company_logo_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "_events_v_version_company_logo_light_id_media_id_fk": { + "name": "_events_v_version_company_logo_light_id_media_id_fk", + "tableFrom": "_events_v", + "tableTo": "media", + "columnsFrom": [ + "version_company_logo_light_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "_events_v_version_meta_image_id_media_id_fk": { + "name": "_events_v_version_meta_image_id_media_id_fk", + "tableFrom": "_events_v", + "tableTo": "media", + "columnsFrom": [ + "version_meta_image_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public._events_v_rels": { + "name": "_events_v_rels", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "order": { + "name": "order", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "parent_id": { + "name": "parent_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "path": { + "name": "path", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "authors_id": { + "name": "authors_id", + "type": "integer", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "_events_v_rels_order_idx": { + "name": "_events_v_rels_order_idx", + "columns": [ + { + "expression": "order", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "_events_v_rels_parent_idx": { + "name": "_events_v_rels_parent_idx", + "columns": [ + { + "expression": "parent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "_events_v_rels_path_idx": { + "name": "_events_v_rels_path_idx", + "columns": [ + { + "expression": "path", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "_events_v_rels_authors_id_idx": { + "name": "_events_v_rels_authors_id_idx", + "columns": [ + { + "expression": "authors_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "_events_v_rels_parent_fk": { + "name": "_events_v_rels_parent_fk", + "tableFrom": "_events_v_rels", + "tableTo": "_events_v", + "columnsFrom": [ + "parent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "_events_v_rels_authors_fk": { + "name": "_events_v_rels_authors_fk", + "tableFrom": "_events_v_rels", + "tableTo": "authors", + "columnsFrom": [ + "authors_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.media": { + "name": "media", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "alt": { + "name": "alt", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "caption": { + "name": "caption", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "prefix": { + "name": "prefix", + "type": "varchar", + "primaryKey": false, + "notNull": false, + "default": "'media'" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "created_at": { + "name": "created_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "url": { + "name": "url", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "thumbnail_u_r_l": { + "name": "thumbnail_u_r_l", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "filename": { + "name": "filename", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "mime_type": { + "name": "mime_type", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "filesize": { + "name": "filesize", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "width": { + "name": "width", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "height": { + "name": "height", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "focal_x": { + "name": "focal_x", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "focal_y": { + "name": "focal_y", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "sizes_thumbnail_url": { + "name": "sizes_thumbnail_url", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "sizes_thumbnail_width": { + "name": "sizes_thumbnail_width", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "sizes_thumbnail_height": { + "name": "sizes_thumbnail_height", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "sizes_thumbnail_mime_type": { + "name": "sizes_thumbnail_mime_type", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "sizes_thumbnail_filesize": { + "name": "sizes_thumbnail_filesize", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "sizes_thumbnail_filename": { + "name": "sizes_thumbnail_filename", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "sizes_square_url": { + "name": "sizes_square_url", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "sizes_square_width": { + "name": "sizes_square_width", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "sizes_square_height": { + "name": "sizes_square_height", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "sizes_square_mime_type": { + "name": "sizes_square_mime_type", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "sizes_square_filesize": { + "name": "sizes_square_filesize", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "sizes_square_filename": { + "name": "sizes_square_filename", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "sizes_small_url": { + "name": "sizes_small_url", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "sizes_small_width": { + "name": "sizes_small_width", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "sizes_small_height": { + "name": "sizes_small_height", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "sizes_small_mime_type": { + "name": "sizes_small_mime_type", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "sizes_small_filesize": { + "name": "sizes_small_filesize", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "sizes_small_filename": { + "name": "sizes_small_filename", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "sizes_medium_url": { + "name": "sizes_medium_url", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "sizes_medium_width": { + "name": "sizes_medium_width", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "sizes_medium_height": { + "name": "sizes_medium_height", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "sizes_medium_mime_type": { + "name": "sizes_medium_mime_type", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "sizes_medium_filesize": { + "name": "sizes_medium_filesize", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "sizes_medium_filename": { + "name": "sizes_medium_filename", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "sizes_large_url": { + "name": "sizes_large_url", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "sizes_large_width": { + "name": "sizes_large_width", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "sizes_large_height": { + "name": "sizes_large_height", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "sizes_large_mime_type": { + "name": "sizes_large_mime_type", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "sizes_large_filesize": { + "name": "sizes_large_filesize", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "sizes_large_filename": { + "name": "sizes_large_filename", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "sizes_xlarge_url": { + "name": "sizes_xlarge_url", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "sizes_xlarge_width": { + "name": "sizes_xlarge_width", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "sizes_xlarge_height": { + "name": "sizes_xlarge_height", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "sizes_xlarge_mime_type": { + "name": "sizes_xlarge_mime_type", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "sizes_xlarge_filesize": { + "name": "sizes_xlarge_filesize", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "sizes_xlarge_filename": { + "name": "sizes_xlarge_filename", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "sizes_og_url": { + "name": "sizes_og_url", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "sizes_og_width": { + "name": "sizes_og_width", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "sizes_og_height": { + "name": "sizes_og_height", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "sizes_og_mime_type": { + "name": "sizes_og_mime_type", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "sizes_og_filesize": { + "name": "sizes_og_filesize", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "sizes_og_filename": { + "name": "sizes_og_filename", + "type": "varchar", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "media_updated_at_idx": { + "name": "media_updated_at_idx", + "columns": [ + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "media_created_at_idx": { + "name": "media_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "media_filename_idx": { + "name": "media_filename_idx", + "columns": [ + { + "expression": "filename", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "media_sizes_thumbnail_sizes_thumbnail_filename_idx": { + "name": "media_sizes_thumbnail_sizes_thumbnail_filename_idx", + "columns": [ + { + "expression": "sizes_thumbnail_filename", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "media_sizes_square_sizes_square_filename_idx": { + "name": "media_sizes_square_sizes_square_filename_idx", + "columns": [ + { + "expression": "sizes_square_filename", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "media_sizes_small_sizes_small_filename_idx": { + "name": "media_sizes_small_sizes_small_filename_idx", + "columns": [ + { + "expression": "sizes_small_filename", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "media_sizes_medium_sizes_medium_filename_idx": { + "name": "media_sizes_medium_sizes_medium_filename_idx", + "columns": [ + { + "expression": "sizes_medium_filename", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "media_sizes_large_sizes_large_filename_idx": { + "name": "media_sizes_large_sizes_large_filename_idx", + "columns": [ + { + "expression": "sizes_large_filename", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "media_sizes_xlarge_sizes_xlarge_filename_idx": { + "name": "media_sizes_xlarge_sizes_xlarge_filename_idx", + "columns": [ + { + "expression": "sizes_xlarge_filename", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "media_sizes_og_sizes_og_filename_idx": { + "name": "media_sizes_og_sizes_og_filename_idx", + "columns": [ + { + "expression": "sizes_og_filename", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.posts": { + "name": "posts", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "title": { + "name": "title", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "slug": { + "name": "slug", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "slug_lock": { + "name": "slug_lock", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": true + }, + "content": { + "name": "content", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "thumb_id": { + "name": "thumb_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "image_id": { + "name": "image_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "launchweek": { + "name": "launchweek", + "type": "enum_posts_launchweek", + "typeSchema": "public", + "primaryKey": false, + "notNull": false + }, + "reading_time": { + "name": "reading_time", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "date": { + "name": "date", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": false + }, + "toc_depth": { + "name": "toc_depth", + "type": "numeric", + "primaryKey": false, + "notNull": false, + "default": 2 + }, + "description": { + "name": "description", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "meta_title": { + "name": "meta_title", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "meta_image_id": { + "name": "meta_image_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "meta_description": { + "name": "meta_description", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "published_at": { + "name": "published_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "created_at": { + "name": "created_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "_status": { + "name": "_status", + "type": "enum_posts_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": false, + "default": "'draft'" + } + }, + "indexes": { + "posts_slug_idx": { + "name": "posts_slug_idx", + "columns": [ + { + "expression": "slug", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "posts_thumb_idx": { + "name": "posts_thumb_idx", + "columns": [ + { + "expression": "thumb_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "posts_image_idx": { + "name": "posts_image_idx", + "columns": [ + { + "expression": "image_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "posts_meta_meta_image_idx": { + "name": "posts_meta_meta_image_idx", + "columns": [ + { + "expression": "meta_image_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "posts_updated_at_idx": { + "name": "posts_updated_at_idx", + "columns": [ + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "posts_created_at_idx": { + "name": "posts_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "posts__status_idx": { + "name": "posts__status_idx", + "columns": [ + { + "expression": "_status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "posts_thumb_id_media_id_fk": { + "name": "posts_thumb_id_media_id_fk", + "tableFrom": "posts", + "tableTo": "media", + "columnsFrom": [ + "thumb_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "posts_image_id_media_id_fk": { + "name": "posts_image_id_media_id_fk", + "tableFrom": "posts", + "tableTo": "media", + "columnsFrom": [ + "image_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "posts_meta_image_id_media_id_fk": { + "name": "posts_meta_image_id_media_id_fk", + "tableFrom": "posts", + "tableTo": "media", + "columnsFrom": [ + "meta_image_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.posts_rels": { + "name": "posts_rels", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "order": { + "name": "order", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "parent_id": { + "name": "parent_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "path": { + "name": "path", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "categories_id": { + "name": "categories_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "authors_id": { + "name": "authors_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "tags_id": { + "name": "tags_id", + "type": "integer", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "posts_rels_order_idx": { + "name": "posts_rels_order_idx", + "columns": [ + { + "expression": "order", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "posts_rels_parent_idx": { + "name": "posts_rels_parent_idx", + "columns": [ + { + "expression": "parent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "posts_rels_path_idx": { + "name": "posts_rels_path_idx", + "columns": [ + { + "expression": "path", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "posts_rels_categories_id_idx": { + "name": "posts_rels_categories_id_idx", + "columns": [ + { + "expression": "categories_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "posts_rels_authors_id_idx": { + "name": "posts_rels_authors_id_idx", + "columns": [ + { + "expression": "authors_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "posts_rels_tags_id_idx": { + "name": "posts_rels_tags_id_idx", + "columns": [ + { + "expression": "tags_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "posts_rels_parent_fk": { + "name": "posts_rels_parent_fk", + "tableFrom": "posts_rels", + "tableTo": "posts", + "columnsFrom": [ + "parent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "posts_rels_categories_fk": { + "name": "posts_rels_categories_fk", + "tableFrom": "posts_rels", + "tableTo": "categories", + "columnsFrom": [ + "categories_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "posts_rels_authors_fk": { + "name": "posts_rels_authors_fk", + "tableFrom": "posts_rels", + "tableTo": "authors", + "columnsFrom": [ + "authors_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "posts_rels_tags_fk": { + "name": "posts_rels_tags_fk", + "tableFrom": "posts_rels", + "tableTo": "tags", + "columnsFrom": [ + "tags_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public._posts_v": { + "name": "_posts_v", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "parent_id": { + "name": "parent_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "version_title": { + "name": "version_title", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "version_slug": { + "name": "version_slug", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "version_slug_lock": { + "name": "version_slug_lock", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": true + }, + "version_content": { + "name": "version_content", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "version_thumb_id": { + "name": "version_thumb_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "version_image_id": { + "name": "version_image_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "version_launchweek": { + "name": "version_launchweek", + "type": "enum__posts_v_version_launchweek", + "typeSchema": "public", + "primaryKey": false, + "notNull": false + }, + "version_reading_time": { + "name": "version_reading_time", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "version_date": { + "name": "version_date", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": false + }, + "version_toc_depth": { + "name": "version_toc_depth", + "type": "numeric", + "primaryKey": false, + "notNull": false, + "default": 2 + }, + "version_description": { + "name": "version_description", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "version_meta_title": { + "name": "version_meta_title", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "version_meta_image_id": { + "name": "version_meta_image_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "version_meta_description": { + "name": "version_meta_description", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "version_published_at": { + "name": "version_published_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": false + }, + "version_updated_at": { + "name": "version_updated_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": false + }, + "version_created_at": { + "name": "version_created_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": false + }, + "version__status": { + "name": "version__status", + "type": "enum__posts_v_version_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": false, + "default": "'draft'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "latest": { + "name": "latest", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "autosave": { + "name": "autosave", + "type": "boolean", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "_posts_v_parent_idx": { + "name": "_posts_v_parent_idx", + "columns": [ + { + "expression": "parent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "_posts_v_version_version_slug_idx": { + "name": "_posts_v_version_version_slug_idx", + "columns": [ + { + "expression": "version_slug", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "_posts_v_version_version_thumb_idx": { + "name": "_posts_v_version_version_thumb_idx", + "columns": [ + { + "expression": "version_thumb_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "_posts_v_version_version_image_idx": { + "name": "_posts_v_version_version_image_idx", + "columns": [ + { + "expression": "version_image_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "_posts_v_version_meta_version_meta_image_idx": { + "name": "_posts_v_version_meta_version_meta_image_idx", + "columns": [ + { + "expression": "version_meta_image_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "_posts_v_version_version_updated_at_idx": { + "name": "_posts_v_version_version_updated_at_idx", + "columns": [ + { + "expression": "version_updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "_posts_v_version_version_created_at_idx": { + "name": "_posts_v_version_version_created_at_idx", + "columns": [ + { + "expression": "version_created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "_posts_v_version_version__status_idx": { + "name": "_posts_v_version_version__status_idx", + "columns": [ + { + "expression": "version__status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "_posts_v_created_at_idx": { + "name": "_posts_v_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "_posts_v_updated_at_idx": { + "name": "_posts_v_updated_at_idx", + "columns": [ + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "_posts_v_latest_idx": { + "name": "_posts_v_latest_idx", + "columns": [ + { + "expression": "latest", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "_posts_v_autosave_idx": { + "name": "_posts_v_autosave_idx", + "columns": [ + { + "expression": "autosave", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "_posts_v_parent_id_posts_id_fk": { + "name": "_posts_v_parent_id_posts_id_fk", + "tableFrom": "_posts_v", + "tableTo": "posts", + "columnsFrom": [ + "parent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "_posts_v_version_thumb_id_media_id_fk": { + "name": "_posts_v_version_thumb_id_media_id_fk", + "tableFrom": "_posts_v", + "tableTo": "media", + "columnsFrom": [ + "version_thumb_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "_posts_v_version_image_id_media_id_fk": { + "name": "_posts_v_version_image_id_media_id_fk", + "tableFrom": "_posts_v", + "tableTo": "media", + "columnsFrom": [ + "version_image_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "_posts_v_version_meta_image_id_media_id_fk": { + "name": "_posts_v_version_meta_image_id_media_id_fk", + "tableFrom": "_posts_v", + "tableTo": "media", + "columnsFrom": [ + "version_meta_image_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public._posts_v_rels": { + "name": "_posts_v_rels", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "order": { + "name": "order", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "parent_id": { + "name": "parent_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "path": { + "name": "path", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "categories_id": { + "name": "categories_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "authors_id": { + "name": "authors_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "tags_id": { + "name": "tags_id", + "type": "integer", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "_posts_v_rels_order_idx": { + "name": "_posts_v_rels_order_idx", + "columns": [ + { + "expression": "order", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "_posts_v_rels_parent_idx": { + "name": "_posts_v_rels_parent_idx", + "columns": [ + { + "expression": "parent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "_posts_v_rels_path_idx": { + "name": "_posts_v_rels_path_idx", + "columns": [ + { + "expression": "path", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "_posts_v_rels_categories_id_idx": { + "name": "_posts_v_rels_categories_id_idx", + "columns": [ + { + "expression": "categories_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "_posts_v_rels_authors_id_idx": { + "name": "_posts_v_rels_authors_id_idx", + "columns": [ + { + "expression": "authors_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "_posts_v_rels_tags_id_idx": { + "name": "_posts_v_rels_tags_id_idx", + "columns": [ + { + "expression": "tags_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "_posts_v_rels_parent_fk": { + "name": "_posts_v_rels_parent_fk", + "tableFrom": "_posts_v_rels", + "tableTo": "_posts_v", + "columnsFrom": [ + "parent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "_posts_v_rels_categories_fk": { + "name": "_posts_v_rels_categories_fk", + "tableFrom": "_posts_v_rels", + "tableTo": "categories", + "columnsFrom": [ + "categories_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "_posts_v_rels_authors_fk": { + "name": "_posts_v_rels_authors_fk", + "tableFrom": "_posts_v_rels", + "tableTo": "authors", + "columnsFrom": [ + "authors_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "_posts_v_rels_tags_fk": { + "name": "_posts_v_rels_tags_fk", + "tableFrom": "_posts_v_rels", + "tableTo": "tags", + "columnsFrom": [ + "tags_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.tags": { + "name": "tags", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "created_at": { + "name": "created_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "tags_updated_at_idx": { + "name": "tags_updated_at_idx", + "columns": [ + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "tags_created_at_idx": { + "name": "tags_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.users_roles": { + "name": "users_roles", + "schema": "", + "columns": { + "order": { + "name": "order", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "parent_id": { + "name": "parent_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "value": { + "name": "value", + "type": "enum_users_roles", + "typeSchema": "public", + "primaryKey": false, + "notNull": false + }, + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + } + }, + "indexes": { + "users_roles_order_idx": { + "name": "users_roles_order_idx", + "columns": [ + { + "expression": "order", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "users_roles_parent_idx": { + "name": "users_roles_parent_idx", + "columns": [ + { + "expression": "parent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "users_roles_parent_fk": { + "name": "users_roles_parent_fk", + "tableFrom": "users_roles", + "tableTo": "users", + "columnsFrom": [ + "parent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.users_sessions": { + "name": "users_sessions", + "schema": "", + "columns": { + "_order": { + "name": "_order", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "_parent_id": { + "name": "_parent_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "id": { + "name": "id", + "type": "varchar", + "primaryKey": true, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": false + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "users_sessions_order_idx": { + "name": "users_sessions_order_idx", + "columns": [ + { + "expression": "_order", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "users_sessions_parent_id_idx": { + "name": "users_sessions_parent_id_idx", + "columns": [ + { + "expression": "_parent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "users_sessions_parent_id_fk": { + "name": "users_sessions_parent_id_fk", + "tableFrom": "users_sessions", + "tableTo": "users", + "columnsFrom": [ + "_parent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.users": { + "name": "users", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "created_at": { + "name": "created_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "email": { + "name": "email", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "reset_password_token": { + "name": "reset_password_token", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "reset_password_expiration": { + "name": "reset_password_expiration", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": false + }, + "salt": { + "name": "salt", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "hash": { + "name": "hash", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "login_attempts": { + "name": "login_attempts", + "type": "numeric", + "primaryKey": false, + "notNull": false, + "default": 0 + }, + "lock_until": { + "name": "lock_until", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "users_updated_at_idx": { + "name": "users_updated_at_idx", + "columns": [ + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "users_created_at_idx": { + "name": "users_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "users_email_idx": { + "name": "users_email_idx", + "columns": [ + { + "expression": "email", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.payload_jobs_log": { + "name": "payload_jobs_log", + "schema": "", + "columns": { + "_order": { + "name": "_order", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "_parent_id": { + "name": "_parent_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "id": { + "name": "id", + "type": "varchar", + "primaryKey": true, + "notNull": true + }, + "executed_at": { + "name": "executed_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": true + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": true + }, + "task_slug": { + "name": "task_slug", + "type": "enum_payload_jobs_log_task_slug", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "task_i_d": { + "name": "task_i_d", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "input": { + "name": "input", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "output": { + "name": "output", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "state": { + "name": "state", + "type": "enum_payload_jobs_log_state", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "error": { + "name": "error", + "type": "jsonb", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "payload_jobs_log_order_idx": { + "name": "payload_jobs_log_order_idx", + "columns": [ + { + "expression": "_order", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "payload_jobs_log_parent_id_idx": { + "name": "payload_jobs_log_parent_id_idx", + "columns": [ + { + "expression": "_parent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "payload_jobs_log_parent_id_fk": { + "name": "payload_jobs_log_parent_id_fk", + "tableFrom": "payload_jobs_log", + "tableTo": "payload_jobs", + "columnsFrom": [ + "_parent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.payload_jobs": { + "name": "payload_jobs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "input": { + "name": "input", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": false + }, + "total_tried": { + "name": "total_tried", + "type": "numeric", + "primaryKey": false, + "notNull": false, + "default": 0 + }, + "has_error": { + "name": "has_error", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "error": { + "name": "error", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "task_slug": { + "name": "task_slug", + "type": "enum_payload_jobs_task_slug", + "typeSchema": "public", + "primaryKey": false, + "notNull": false + }, + "queue": { + "name": "queue", + "type": "varchar", + "primaryKey": false, + "notNull": false, + "default": "'default'" + }, + "wait_until": { + "name": "wait_until", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": false + }, + "processing": { + "name": "processing", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "created_at": { + "name": "created_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "payload_jobs_completed_at_idx": { + "name": "payload_jobs_completed_at_idx", + "columns": [ + { + "expression": "completed_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "payload_jobs_total_tried_idx": { + "name": "payload_jobs_total_tried_idx", + "columns": [ + { + "expression": "total_tried", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "payload_jobs_has_error_idx": { + "name": "payload_jobs_has_error_idx", + "columns": [ + { + "expression": "has_error", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "payload_jobs_task_slug_idx": { + "name": "payload_jobs_task_slug_idx", + "columns": [ + { + "expression": "task_slug", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "payload_jobs_queue_idx": { + "name": "payload_jobs_queue_idx", + "columns": [ + { + "expression": "queue", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "payload_jobs_wait_until_idx": { + "name": "payload_jobs_wait_until_idx", + "columns": [ + { + "expression": "wait_until", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "payload_jobs_processing_idx": { + "name": "payload_jobs_processing_idx", + "columns": [ + { + "expression": "processing", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "payload_jobs_updated_at_idx": { + "name": "payload_jobs_updated_at_idx", + "columns": [ + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "payload_jobs_created_at_idx": { + "name": "payload_jobs_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.payload_locked_documents": { + "name": "payload_locked_documents", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "global_slug": { + "name": "global_slug", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "created_at": { + "name": "created_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "payload_locked_documents_global_slug_idx": { + "name": "payload_locked_documents_global_slug_idx", + "columns": [ + { + "expression": "global_slug", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "payload_locked_documents_updated_at_idx": { + "name": "payload_locked_documents_updated_at_idx", + "columns": [ + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "payload_locked_documents_created_at_idx": { + "name": "payload_locked_documents_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.payload_locked_documents_rels": { + "name": "payload_locked_documents_rels", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "order": { + "name": "order", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "parent_id": { + "name": "parent_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "path": { + "name": "path", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "authors_id": { + "name": "authors_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "categories_id": { + "name": "categories_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "customers_id": { + "name": "customers_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "events_id": { + "name": "events_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "media_id": { + "name": "media_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "posts_id": { + "name": "posts_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "tags_id": { + "name": "tags_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "users_id": { + "name": "users_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "payload_jobs_id": { + "name": "payload_jobs_id", + "type": "integer", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "payload_locked_documents_rels_order_idx": { + "name": "payload_locked_documents_rels_order_idx", + "columns": [ + { + "expression": "order", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "payload_locked_documents_rels_parent_idx": { + "name": "payload_locked_documents_rels_parent_idx", + "columns": [ + { + "expression": "parent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "payload_locked_documents_rels_path_idx": { + "name": "payload_locked_documents_rels_path_idx", + "columns": [ + { + "expression": "path", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "payload_locked_documents_rels_authors_id_idx": { + "name": "payload_locked_documents_rels_authors_id_idx", + "columns": [ + { + "expression": "authors_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "payload_locked_documents_rels_categories_id_idx": { + "name": "payload_locked_documents_rels_categories_id_idx", + "columns": [ + { + "expression": "categories_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "payload_locked_documents_rels_customers_id_idx": { + "name": "payload_locked_documents_rels_customers_id_idx", + "columns": [ + { + "expression": "customers_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "payload_locked_documents_rels_events_id_idx": { + "name": "payload_locked_documents_rels_events_id_idx", + "columns": [ + { + "expression": "events_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "payload_locked_documents_rels_media_id_idx": { + "name": "payload_locked_documents_rels_media_id_idx", + "columns": [ + { + "expression": "media_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "payload_locked_documents_rels_posts_id_idx": { + "name": "payload_locked_documents_rels_posts_id_idx", + "columns": [ + { + "expression": "posts_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "payload_locked_documents_rels_tags_id_idx": { + "name": "payload_locked_documents_rels_tags_id_idx", + "columns": [ + { + "expression": "tags_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "payload_locked_documents_rels_users_id_idx": { + "name": "payload_locked_documents_rels_users_id_idx", + "columns": [ + { + "expression": "users_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "payload_locked_documents_rels_payload_jobs_id_idx": { + "name": "payload_locked_documents_rels_payload_jobs_id_idx", + "columns": [ + { + "expression": "payload_jobs_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "payload_locked_documents_rels_parent_fk": { + "name": "payload_locked_documents_rels_parent_fk", + "tableFrom": "payload_locked_documents_rels", + "tableTo": "payload_locked_documents", + "columnsFrom": [ + "parent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "payload_locked_documents_rels_authors_fk": { + "name": "payload_locked_documents_rels_authors_fk", + "tableFrom": "payload_locked_documents_rels", + "tableTo": "authors", + "columnsFrom": [ + "authors_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "payload_locked_documents_rels_categories_fk": { + "name": "payload_locked_documents_rels_categories_fk", + "tableFrom": "payload_locked_documents_rels", + "tableTo": "categories", + "columnsFrom": [ + "categories_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "payload_locked_documents_rels_customers_fk": { + "name": "payload_locked_documents_rels_customers_fk", + "tableFrom": "payload_locked_documents_rels", + "tableTo": "customers", + "columnsFrom": [ + "customers_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "payload_locked_documents_rels_events_fk": { + "name": "payload_locked_documents_rels_events_fk", + "tableFrom": "payload_locked_documents_rels", + "tableTo": "events", + "columnsFrom": [ + "events_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "payload_locked_documents_rels_media_fk": { + "name": "payload_locked_documents_rels_media_fk", + "tableFrom": "payload_locked_documents_rels", + "tableTo": "media", + "columnsFrom": [ + "media_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "payload_locked_documents_rels_posts_fk": { + "name": "payload_locked_documents_rels_posts_fk", + "tableFrom": "payload_locked_documents_rels", + "tableTo": "posts", + "columnsFrom": [ + "posts_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "payload_locked_documents_rels_tags_fk": { + "name": "payload_locked_documents_rels_tags_fk", + "tableFrom": "payload_locked_documents_rels", + "tableTo": "tags", + "columnsFrom": [ + "tags_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "payload_locked_documents_rels_users_fk": { + "name": "payload_locked_documents_rels_users_fk", + "tableFrom": "payload_locked_documents_rels", + "tableTo": "users", + "columnsFrom": [ + "users_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "payload_locked_documents_rels_payload_jobs_fk": { + "name": "payload_locked_documents_rels_payload_jobs_fk", + "tableFrom": "payload_locked_documents_rels", + "tableTo": "payload_jobs", + "columnsFrom": [ + "payload_jobs_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.payload_preferences": { + "name": "payload_preferences", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "key": { + "name": "key", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "value": { + "name": "value", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "created_at": { + "name": "created_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "payload_preferences_key_idx": { + "name": "payload_preferences_key_idx", + "columns": [ + { + "expression": "key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "payload_preferences_updated_at_idx": { + "name": "payload_preferences_updated_at_idx", + "columns": [ + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "payload_preferences_created_at_idx": { + "name": "payload_preferences_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.payload_preferences_rels": { + "name": "payload_preferences_rels", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "order": { + "name": "order", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "parent_id": { + "name": "parent_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "path": { + "name": "path", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "users_id": { + "name": "users_id", + "type": "integer", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "payload_preferences_rels_order_idx": { + "name": "payload_preferences_rels_order_idx", + "columns": [ + { + "expression": "order", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "payload_preferences_rels_parent_idx": { + "name": "payload_preferences_rels_parent_idx", + "columns": [ + { + "expression": "parent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "payload_preferences_rels_path_idx": { + "name": "payload_preferences_rels_path_idx", + "columns": [ + { + "expression": "path", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "payload_preferences_rels_users_id_idx": { + "name": "payload_preferences_rels_users_id_idx", + "columns": [ + { + "expression": "users_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "payload_preferences_rels_parent_fk": { + "name": "payload_preferences_rels_parent_fk", + "tableFrom": "payload_preferences_rels", + "tableTo": "payload_preferences", + "columnsFrom": [ + "parent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "payload_preferences_rels_users_fk": { + "name": "payload_preferences_rels_users_fk", + "tableFrom": "payload_preferences_rels", + "tableTo": "users", + "columnsFrom": [ + "users_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.payload_migrations": { + "name": "payload_migrations", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "batch": { + "name": "batch", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "created_at": { + "name": "created_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "payload_migrations_updated_at_idx": { + "name": "payload_migrations_updated_at_idx", + "columns": [ + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "payload_migrations_created_at_idx": { + "name": "payload_migrations_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + } + }, + "enums": { + "public.enum_customers_industry": { + "name": "enum_customers_industry", + "schema": "public", + "values": [ + "healthcare", + "fintech", + "ecommerce", + "education", + "gaming", + "media", + "real-estate", + "saas", + "social", + "analytics", + "ai", + "developer-tools" + ] + }, + "public.enum_customers_supabase_products": { + "name": "enum_customers_supabase_products", + "schema": "public", + "values": [ + "database", + "auth", + "storage", + "realtime", + "functions", + "vector" + ] + }, + "public.enum_customers_company_size": { + "name": "enum_customers_company_size", + "schema": "public", + "values": [ + "startup", + "enterprise", + "indie_dev" + ] + }, + "public.enum_customers_region": { + "name": "enum_customers_region", + "schema": "public", + "values": [ + "Asia", + "Europe", + "North America", + "South America", + "Africa", + "Oceania" + ] + }, + "public.enum_customers_status": { + "name": "enum_customers_status", + "schema": "public", + "values": [ + "draft", + "published" + ] + }, + "public.enum__customers_v_version_industry": { + "name": "enum__customers_v_version_industry", + "schema": "public", + "values": [ + "healthcare", + "fintech", + "ecommerce", + "education", + "gaming", + "media", + "real-estate", + "saas", + "social", + "analytics", + "ai", + "developer-tools" + ] + }, + "public.enum__customers_v_version_supabase_products": { + "name": "enum__customers_v_version_supabase_products", + "schema": "public", + "values": [ + "database", + "auth", + "storage", + "realtime", + "functions", + "vector" + ] + }, + "public.enum__customers_v_version_company_size": { + "name": "enum__customers_v_version_company_size", + "schema": "public", + "values": [ + "startup", + "enterprise", + "indie_dev" + ] + }, + "public.enum__customers_v_version_region": { + "name": "enum__customers_v_version_region", + "schema": "public", + "values": [ + "Asia", + "Europe", + "North America", + "South America", + "Africa", + "Oceania" + ] + }, + "public.enum__customers_v_version_status": { + "name": "enum__customers_v_version_status", + "schema": "public", + "values": [ + "draft", + "published" + ] + }, + "public.enum_events_type": { + "name": "enum_events_type", + "schema": "public", + "values": [ + "conference", + "hackathon", + "launch-week", + "meetup", + "office-hours", + "talk", + "webinar", + "workshop", + "other" + ] + }, + "public.enum_events_timezone": { + "name": "enum_events_timezone", + "schema": "public", + "values": [ + "Africa/Abidjan", + "Africa/Accra", + "Africa/Addis_Ababa", + "Africa/Algiers", + "Africa/Asmara", + "Africa/Bamako", + "Africa/Bangui", + "Africa/Banjul", + "Africa/Bissau", + "Africa/Blantyre", + "Africa/Brazzaville", + "Africa/Bujumbura", + "Africa/Cairo", + "Africa/Casablanca", + "Africa/Ceuta", + "Africa/Conakry", + "Africa/Dakar", + "Africa/Dar_es_Salaam", + "Africa/Djibouti", + "Africa/Douala", + "Africa/El_Aaiun", + "Africa/Freetown", + "Africa/Gaborone", + "Africa/Harare", + "Africa/Johannesburg", + "Africa/Juba", + "Africa/Kampala", + "Africa/Khartoum", + "Africa/Kigali", + "Africa/Kinshasa", + "Africa/Lagos", + "Africa/Libreville", + "Africa/Lome", + "Africa/Luanda", + "Africa/Lubumbashi", + "Africa/Lusaka", + "Africa/Malabo", + "Africa/Maputo", + "Africa/Maseru", + "Africa/Mbabane", + "Africa/Mogadishu", + "Africa/Monrovia", + "Africa/Nairobi", + "Africa/Ndjamena", + "Africa/Niamey", + "Africa/Nouakchott", + "Africa/Ouagadougou", + "Africa/Porto-Novo", + "Africa/Sao_Tome", + "Africa/Tripoli", + "Africa/Tunis", + "Africa/Windhoek", + "America/Adak", + "America/Anchorage", + "America/Anguilla", + "America/Antigua", + "America/Araguaina", + "America/Argentina/Buenos_Aires", + "America/Argentina/Catamarca", + "America/Argentina/Cordoba", + "America/Argentina/Jujuy", + "America/Argentina/La_Rioja", + "America/Argentina/Mendoza", + "America/Argentina/Rio_Gallegos", + "America/Argentina/Salta", + "America/Argentina/San_Juan", + "America/Argentina/San_Luis", + "America/Argentina/Tucuman", + "America/Argentina/Ushuaia", + "America/Aruba", + "America/Asuncion", + "America/Atikokan", + "America/Bahia", + "America/Bahia_Banderas", + "America/Barbados", + "America/Belem", + "America/Belize", + "America/Blanc-Sablon", + "America/Boa_Vista", + "America/Bogota", + "America/Boise", + "America/Cambridge_Bay", + "America/Campo_Grande", + "America/Cancun", + "America/Caracas", + "America/Cayenne", + "America/Cayman", + "America/Chicago", + "America/Chihuahua", + "America/Costa_Rica", + "America/Creston", + "America/Cuiaba", + "America/Curacao", + "America/Danmarkshavn", + "America/Dawson", + "America/Dawson_Creek", + "America/Denver", + "America/Detroit", + "America/Dominica", + "America/Edmonton", + "America/Eirunepe", + "America/El_Salvador", + "America/Fort_Nelson", + "America/Fortaleza", + "America/Glace_Bay", + "America/Godthab", + "America/Goose_Bay", + "America/Grand_Turk", + "America/Grenada", + "America/Guadeloupe", + "America/Guatemala", + "America/Guayaquil", + "America/Guyana", + "America/Halifax", + "America/Havana", + "America/Hermosillo", + "America/Indiana/Indianapolis", + "America/Indiana/Knox", + "America/Indiana/Marengo", + "America/Indiana/Petersburg", + "America/Indiana/Tell_City", + "America/Indiana/Vevay", + "America/Indiana/Vincennes", + "America/Indiana/Winamac", + "America/Inuvik", + "America/Iqaluit", + "America/Jamaica", + "America/Juneau", + "America/Kentucky/Louisville", + "America/Kentucky/Monticello", + "America/Kralendijk", + "America/La_Paz", + "America/Lima", + "America/Los_Angeles", + "America/Lower_Princes", + "America/Maceio", + "America/Managua", + "America/Manaus", + "America/Marigot", + "America/Martinique", + "America/Matamoros", + "America/Mazatlan", + "America/Menominee", + "America/Merida", + "America/Metlakatla", + "America/Mexico_City", + "America/Miquelon", + "America/Moncton", + "America/Monterrey", + "America/Montevideo", + "America/Montserrat", + "America/Nassau", + "America/New_York", + "America/Nipigon", + "America/Nome", + "America/Noronha", + "America/North_Dakota/Beulah", + "America/North_Dakota/Center", + "America/North_Dakota/New_Salem", + "America/Ojinaga", + "America/Panama", + "America/Pangnirtung", + "America/Paramaribo", + "America/Phoenix", + "America/Port-au-Prince", + "America/Port_of_Spain", + "America/Porto_Velho", + "America/Puerto_Rico", + "America/Punta_Arenas", + "America/Rainy_River", + "America/Rankin_Inlet", + "America/Recife", + "America/Regina", + "America/Resolute", + "America/Rio_Branco", + "America/Santarem", + "America/Santiago", + "America/Santo_Domingo", + "America/Sao_Paulo", + "America/Scoresbysund", + "America/Sitka", + "America/St_Barthelemy", + "America/St_Johns", + "America/St_Kitts", + "America/St_Lucia", + "America/St_Thomas", + "America/St_Vincent", + "America/Swift_Current", + "America/Tegucigalpa", + "America/Thule", + "America/Thunder_Bay", + "America/Tijuana", + "America/Toronto", + "America/Tortola", + "America/Vancouver", + "America/Whitehorse", + "America/Winnipeg", + "America/Yakutat", + "America/Yellowknife", + "Antarctica/Casey", + "Antarctica/Davis", + "Antarctica/DumontDUrville", + "Antarctica/Macquarie", + "Antarctica/Mawson", + "Antarctica/McMurdo", + "Antarctica/Palmer", + "Antarctica/Rothera", + "Antarctica/Syowa", + "Antarctica/Troll", + "Antarctica/Vostok", + "Arctic/Longyearbyen", + "Asia/Aden", + "Asia/Almaty", + "Asia/Amman", + "Asia/Anadyr", + "Asia/Aqtau", + "Asia/Aqtobe", + "Asia/Ashgabat", + "Asia/Atyrau", + "Asia/Baghdad", + "Asia/Bahrain", + "Asia/Baku", + "Asia/Bangkok", + "Asia/Barnaul", + "Asia/Beirut", + "Asia/Bishkek", + "Asia/Brunei", + "Asia/Chita", + "Asia/Choibalsan", + "Asia/Colombo", + "Asia/Damascus", + "Asia/Dhaka", + "Asia/Dili", + "Asia/Dubai", + "Asia/Dushanbe", + "Asia/Famagusta", + "Asia/Gaza", + "Asia/Hebron", + "Asia/Ho_Chi_Minh", + "Asia/Hong_Kong", + "Asia/Hovd", + "Asia/Irkutsk", + "Asia/Istanbul", + "Asia/Jakarta", + "Asia/Jayapura", + "Asia/Jerusalem", + "Asia/Kabul", + "Asia/Kamchatka", + "Asia/Karachi", + "Asia/Kathmandu", + "Asia/Khandyga", + "Asia/Kolkata", + "Asia/Krasnoyarsk", + "Asia/Kuala_Lumpur", + "Asia/Kuching", + "Asia/Kuwait", + "Asia/Macau", + "Asia/Magadan", + "Asia/Makassar", + "Asia/Manila", + "Asia/Muscat", + "Asia/Nicosia", + "Asia/Novokuznetsk", + "Asia/Novosibirsk", + "Asia/Omsk", + "Asia/Oral", + "Asia/Phnom_Penh", + "Asia/Pontianak", + "Asia/Pyongyang", + "Asia/Qatar", + "Asia/Qostanay", + "Asia/Qyzylorda", + "Asia/Riyadh", + "Asia/Sakhalin", + "Asia/Samarkand", + "Asia/Seoul", + "Asia/Shanghai", + "Asia/Singapore", + "Asia/Srednekolymsk", + "Asia/Taipei", + "Asia/Tashkent", + "Asia/Tbilisi", + "Asia/Tehran", + "Asia/Thimphu", + "Asia/Tokyo", + "Asia/Tomsk", + "Asia/Ulaanbaatar", + "Asia/Urumqi", + "Asia/Ust-Nera", + "Asia/Vientiane", + "Asia/Vladivostok", + "Asia/Yakutsk", + "Asia/Yangon", + "Asia/Yekaterinburg", + "Asia/Yerevan", + "Atlantic/Azores", + "Atlantic/Bermuda", + "Atlantic/Canary", + "Atlantic/Cape_Verde", + "Atlantic/Faroe", + "Atlantic/Madeira", + "Atlantic/Reykjavik", + "Atlantic/South_Georgia", + "Atlantic/St_Helena", + "Atlantic/Stanley", + "Australia/Adelaide", + "Australia/Brisbane", + "Australia/Broken_Hill", + "Australia/Currie", + "Australia/Darwin", + "Australia/Eucla", + "Australia/Hobart", + "Australia/Lindeman", + "Australia/Lord_Howe", + "Australia/Melbourne", + "Australia/Perth", + "Australia/Sydney", + "Europe/Amsterdam", + "Europe/Andorra", + "Europe/Astrakhan", + "Europe/Athens", + "Europe/Belgrade", + "Europe/Berlin", + "Europe/Bratislava", + "Europe/Brussels", + "Europe/Bucharest", + "Europe/Budapest", + "Europe/Busingen", + "Europe/Chisinau", + "Europe/Copenhagen", + "Europe/Dublin", + "Europe/Gibraltar", + "Europe/Guernsey", + "Europe/Helsinki", + "Europe/Isle_of_Man", + "Europe/Istanbul", + "Europe/Jersey", + "Europe/Kaliningrad", + "Europe/Kiev", + "Europe/Kirov", + "Europe/Lisbon", + "Europe/Ljubljana", + "Europe/London", + "Europe/Luxembourg", + "Europe/Madrid", + "Europe/Malta", + "Europe/Mariehamn", + "Europe/Minsk", + "Europe/Monaco", + "Europe/Moscow", + "Europe/Oslo", + "Europe/Paris", + "Europe/Podgorica", + "Europe/Prague", + "Europe/Riga", + "Europe/Rome", + "Europe/Samara", + "Europe/San_Marino", + "Europe/Sarajevo", + "Europe/Saratov", + "Europe/Simferopol", + "Europe/Skopje", + "Europe/Sofia", + "Europe/Stockholm", + "Europe/Tallinn", + "Europe/Tirane", + "Europe/Ulyanovsk", + "Europe/Uzhgorod", + "Europe/Vaduz", + "Europe/Vatican", + "Europe/Vienna", + "Europe/Vilnius", + "Europe/Volgograd", + "Europe/Warsaw", + "Europe/Zagreb", + "Europe/Zaporozhye", + "Europe/Zurich", + "Indian/Antananarivo", + "Indian/Chagos", + "Indian/Christmas", + "Indian/Cocos", + "Indian/Comoro", + "Indian/Kerguelen", + "Indian/Mahe", + "Indian/Maldives", + "Indian/Mauritius", + "Indian/Mayotte", + "Indian/Reunion", + "Pacific/Apia", + "Pacific/Auckland", + "Pacific/Bougainville", + "Pacific/Chatham", + "Pacific/Chuuk", + "Pacific/Easter", + "Pacific/Efate", + "Pacific/Enderbury", + "Pacific/Fakaofo", + "Pacific/Fiji", + "Pacific/Funafuti", + "Pacific/Galapagos", + "Pacific/Gambier", + "Pacific/Guadalcanal", + "Pacific/Guam", + "Pacific/Honolulu", + "Pacific/Kiritimati", + "Pacific/Kosrae", + "Pacific/Kwajalein", + "Pacific/Majuro", + "Pacific/Marquesas", + "Pacific/Midway", + "Pacific/Nauru", + "Pacific/Niue", + "Pacific/Norfolk", + "Pacific/Noumea", + "Pacific/Pago_Pago", + "Pacific/Palau", + "Pacific/Pitcairn", + "Pacific/Pohnpei", + "Pacific/Port_Moresby", + "Pacific/Rarotonga", + "Pacific/Saipan", + "Pacific/Tahiti", + "Pacific/Tarawa", + "Pacific/Tongatapu", + "Pacific/Wake", + "Pacific/Wallis", + "UTC" + ] + }, + "public.enum_events_link_target": { + "name": "enum_events_link_target", + "schema": "public", + "values": [ + "_self", + "_blank" + ] + }, + "public.enum_events_main_cta_target": { + "name": "enum_events_main_cta_target", + "schema": "public", + "values": [ + "_self", + "_blank" + ] + }, + "public.enum_events_status": { + "name": "enum_events_status", + "schema": "public", + "values": [ + "draft", + "published" + ] + }, + "public.enum__events_v_version_type": { + "name": "enum__events_v_version_type", + "schema": "public", + "values": [ + "conference", + "hackathon", + "launch-week", + "meetup", + "office-hours", + "talk", + "webinar", + "workshop", + "other" + ] + }, + "public.enum__events_v_version_timezone": { + "name": "enum__events_v_version_timezone", + "schema": "public", + "values": [ + "Africa/Abidjan", + "Africa/Accra", + "Africa/Addis_Ababa", + "Africa/Algiers", + "Africa/Asmara", + "Africa/Bamako", + "Africa/Bangui", + "Africa/Banjul", + "Africa/Bissau", + "Africa/Blantyre", + "Africa/Brazzaville", + "Africa/Bujumbura", + "Africa/Cairo", + "Africa/Casablanca", + "Africa/Ceuta", + "Africa/Conakry", + "Africa/Dakar", + "Africa/Dar_es_Salaam", + "Africa/Djibouti", + "Africa/Douala", + "Africa/El_Aaiun", + "Africa/Freetown", + "Africa/Gaborone", + "Africa/Harare", + "Africa/Johannesburg", + "Africa/Juba", + "Africa/Kampala", + "Africa/Khartoum", + "Africa/Kigali", + "Africa/Kinshasa", + "Africa/Lagos", + "Africa/Libreville", + "Africa/Lome", + "Africa/Luanda", + "Africa/Lubumbashi", + "Africa/Lusaka", + "Africa/Malabo", + "Africa/Maputo", + "Africa/Maseru", + "Africa/Mbabane", + "Africa/Mogadishu", + "Africa/Monrovia", + "Africa/Nairobi", + "Africa/Ndjamena", + "Africa/Niamey", + "Africa/Nouakchott", + "Africa/Ouagadougou", + "Africa/Porto-Novo", + "Africa/Sao_Tome", + "Africa/Tripoli", + "Africa/Tunis", + "Africa/Windhoek", + "America/Adak", + "America/Anchorage", + "America/Anguilla", + "America/Antigua", + "America/Araguaina", + "America/Argentina/Buenos_Aires", + "America/Argentina/Catamarca", + "America/Argentina/Cordoba", + "America/Argentina/Jujuy", + "America/Argentina/La_Rioja", + "America/Argentina/Mendoza", + "America/Argentina/Rio_Gallegos", + "America/Argentina/Salta", + "America/Argentina/San_Juan", + "America/Argentina/San_Luis", + "America/Argentina/Tucuman", + "America/Argentina/Ushuaia", + "America/Aruba", + "America/Asuncion", + "America/Atikokan", + "America/Bahia", + "America/Bahia_Banderas", + "America/Barbados", + "America/Belem", + "America/Belize", + "America/Blanc-Sablon", + "America/Boa_Vista", + "America/Bogota", + "America/Boise", + "America/Cambridge_Bay", + "America/Campo_Grande", + "America/Cancun", + "America/Caracas", + "America/Cayenne", + "America/Cayman", + "America/Chicago", + "America/Chihuahua", + "America/Costa_Rica", + "America/Creston", + "America/Cuiaba", + "America/Curacao", + "America/Danmarkshavn", + "America/Dawson", + "America/Dawson_Creek", + "America/Denver", + "America/Detroit", + "America/Dominica", + "America/Edmonton", + "America/Eirunepe", + "America/El_Salvador", + "America/Fort_Nelson", + "America/Fortaleza", + "America/Glace_Bay", + "America/Godthab", + "America/Goose_Bay", + "America/Grand_Turk", + "America/Grenada", + "America/Guadeloupe", + "America/Guatemala", + "America/Guayaquil", + "America/Guyana", + "America/Halifax", + "America/Havana", + "America/Hermosillo", + "America/Indiana/Indianapolis", + "America/Indiana/Knox", + "America/Indiana/Marengo", + "America/Indiana/Petersburg", + "America/Indiana/Tell_City", + "America/Indiana/Vevay", + "America/Indiana/Vincennes", + "America/Indiana/Winamac", + "America/Inuvik", + "America/Iqaluit", + "America/Jamaica", + "America/Juneau", + "America/Kentucky/Louisville", + "America/Kentucky/Monticello", + "America/Kralendijk", + "America/La_Paz", + "America/Lima", + "America/Los_Angeles", + "America/Lower_Princes", + "America/Maceio", + "America/Managua", + "America/Manaus", + "America/Marigot", + "America/Martinique", + "America/Matamoros", + "America/Mazatlan", + "America/Menominee", + "America/Merida", + "America/Metlakatla", + "America/Mexico_City", + "America/Miquelon", + "America/Moncton", + "America/Monterrey", + "America/Montevideo", + "America/Montserrat", + "America/Nassau", + "America/New_York", + "America/Nipigon", + "America/Nome", + "America/Noronha", + "America/North_Dakota/Beulah", + "America/North_Dakota/Center", + "America/North_Dakota/New_Salem", + "America/Ojinaga", + "America/Panama", + "America/Pangnirtung", + "America/Paramaribo", + "America/Phoenix", + "America/Port-au-Prince", + "America/Port_of_Spain", + "America/Porto_Velho", + "America/Puerto_Rico", + "America/Punta_Arenas", + "America/Rainy_River", + "America/Rankin_Inlet", + "America/Recife", + "America/Regina", + "America/Resolute", + "America/Rio_Branco", + "America/Santarem", + "America/Santiago", + "America/Santo_Domingo", + "America/Sao_Paulo", + "America/Scoresbysund", + "America/Sitka", + "America/St_Barthelemy", + "America/St_Johns", + "America/St_Kitts", + "America/St_Lucia", + "America/St_Thomas", + "America/St_Vincent", + "America/Swift_Current", + "America/Tegucigalpa", + "America/Thule", + "America/Thunder_Bay", + "America/Tijuana", + "America/Toronto", + "America/Tortola", + "America/Vancouver", + "America/Whitehorse", + "America/Winnipeg", + "America/Yakutat", + "America/Yellowknife", + "Antarctica/Casey", + "Antarctica/Davis", + "Antarctica/DumontDUrville", + "Antarctica/Macquarie", + "Antarctica/Mawson", + "Antarctica/McMurdo", + "Antarctica/Palmer", + "Antarctica/Rothera", + "Antarctica/Syowa", + "Antarctica/Troll", + "Antarctica/Vostok", + "Arctic/Longyearbyen", + "Asia/Aden", + "Asia/Almaty", + "Asia/Amman", + "Asia/Anadyr", + "Asia/Aqtau", + "Asia/Aqtobe", + "Asia/Ashgabat", + "Asia/Atyrau", + "Asia/Baghdad", + "Asia/Bahrain", + "Asia/Baku", + "Asia/Bangkok", + "Asia/Barnaul", + "Asia/Beirut", + "Asia/Bishkek", + "Asia/Brunei", + "Asia/Chita", + "Asia/Choibalsan", + "Asia/Colombo", + "Asia/Damascus", + "Asia/Dhaka", + "Asia/Dili", + "Asia/Dubai", + "Asia/Dushanbe", + "Asia/Famagusta", + "Asia/Gaza", + "Asia/Hebron", + "Asia/Ho_Chi_Minh", + "Asia/Hong_Kong", + "Asia/Hovd", + "Asia/Irkutsk", + "Asia/Istanbul", + "Asia/Jakarta", + "Asia/Jayapura", + "Asia/Jerusalem", + "Asia/Kabul", + "Asia/Kamchatka", + "Asia/Karachi", + "Asia/Kathmandu", + "Asia/Khandyga", + "Asia/Kolkata", + "Asia/Krasnoyarsk", + "Asia/Kuala_Lumpur", + "Asia/Kuching", + "Asia/Kuwait", + "Asia/Macau", + "Asia/Magadan", + "Asia/Makassar", + "Asia/Manila", + "Asia/Muscat", + "Asia/Nicosia", + "Asia/Novokuznetsk", + "Asia/Novosibirsk", + "Asia/Omsk", + "Asia/Oral", + "Asia/Phnom_Penh", + "Asia/Pontianak", + "Asia/Pyongyang", + "Asia/Qatar", + "Asia/Qostanay", + "Asia/Qyzylorda", + "Asia/Riyadh", + "Asia/Sakhalin", + "Asia/Samarkand", + "Asia/Seoul", + "Asia/Shanghai", + "Asia/Singapore", + "Asia/Srednekolymsk", + "Asia/Taipei", + "Asia/Tashkent", + "Asia/Tbilisi", + "Asia/Tehran", + "Asia/Thimphu", + "Asia/Tokyo", + "Asia/Tomsk", + "Asia/Ulaanbaatar", + "Asia/Urumqi", + "Asia/Ust-Nera", + "Asia/Vientiane", + "Asia/Vladivostok", + "Asia/Yakutsk", + "Asia/Yangon", + "Asia/Yekaterinburg", + "Asia/Yerevan", + "Atlantic/Azores", + "Atlantic/Bermuda", + "Atlantic/Canary", + "Atlantic/Cape_Verde", + "Atlantic/Faroe", + "Atlantic/Madeira", + "Atlantic/Reykjavik", + "Atlantic/South_Georgia", + "Atlantic/St_Helena", + "Atlantic/Stanley", + "Australia/Adelaide", + "Australia/Brisbane", + "Australia/Broken_Hill", + "Australia/Currie", + "Australia/Darwin", + "Australia/Eucla", + "Australia/Hobart", + "Australia/Lindeman", + "Australia/Lord_Howe", + "Australia/Melbourne", + "Australia/Perth", + "Australia/Sydney", + "Europe/Amsterdam", + "Europe/Andorra", + "Europe/Astrakhan", + "Europe/Athens", + "Europe/Belgrade", + "Europe/Berlin", + "Europe/Bratislava", + "Europe/Brussels", + "Europe/Bucharest", + "Europe/Budapest", + "Europe/Busingen", + "Europe/Chisinau", + "Europe/Copenhagen", + "Europe/Dublin", + "Europe/Gibraltar", + "Europe/Guernsey", + "Europe/Helsinki", + "Europe/Isle_of_Man", + "Europe/Istanbul", + "Europe/Jersey", + "Europe/Kaliningrad", + "Europe/Kiev", + "Europe/Kirov", + "Europe/Lisbon", + "Europe/Ljubljana", + "Europe/London", + "Europe/Luxembourg", + "Europe/Madrid", + "Europe/Malta", + "Europe/Mariehamn", + "Europe/Minsk", + "Europe/Monaco", + "Europe/Moscow", + "Europe/Oslo", + "Europe/Paris", + "Europe/Podgorica", + "Europe/Prague", + "Europe/Riga", + "Europe/Rome", + "Europe/Samara", + "Europe/San_Marino", + "Europe/Sarajevo", + "Europe/Saratov", + "Europe/Simferopol", + "Europe/Skopje", + "Europe/Sofia", + "Europe/Stockholm", + "Europe/Tallinn", + "Europe/Tirane", + "Europe/Ulyanovsk", + "Europe/Uzhgorod", + "Europe/Vaduz", + "Europe/Vatican", + "Europe/Vienna", + "Europe/Vilnius", + "Europe/Volgograd", + "Europe/Warsaw", + "Europe/Zagreb", + "Europe/Zaporozhye", + "Europe/Zurich", + "Indian/Antananarivo", + "Indian/Chagos", + "Indian/Christmas", + "Indian/Cocos", + "Indian/Comoro", + "Indian/Kerguelen", + "Indian/Mahe", + "Indian/Maldives", + "Indian/Mauritius", + "Indian/Mayotte", + "Indian/Reunion", + "Pacific/Apia", + "Pacific/Auckland", + "Pacific/Bougainville", + "Pacific/Chatham", + "Pacific/Chuuk", + "Pacific/Easter", + "Pacific/Efate", + "Pacific/Enderbury", + "Pacific/Fakaofo", + "Pacific/Fiji", + "Pacific/Funafuti", + "Pacific/Galapagos", + "Pacific/Gambier", + "Pacific/Guadalcanal", + "Pacific/Guam", + "Pacific/Honolulu", + "Pacific/Kiritimati", + "Pacific/Kosrae", + "Pacific/Kwajalein", + "Pacific/Majuro", + "Pacific/Marquesas", + "Pacific/Midway", + "Pacific/Nauru", + "Pacific/Niue", + "Pacific/Norfolk", + "Pacific/Noumea", + "Pacific/Pago_Pago", + "Pacific/Palau", + "Pacific/Pitcairn", + "Pacific/Pohnpei", + "Pacific/Port_Moresby", + "Pacific/Rarotonga", + "Pacific/Saipan", + "Pacific/Tahiti", + "Pacific/Tarawa", + "Pacific/Tongatapu", + "Pacific/Wake", + "Pacific/Wallis", + "UTC" + ] + }, + "public.enum__events_v_version_link_target": { + "name": "enum__events_v_version_link_target", + "schema": "public", + "values": [ + "_self", + "_blank" + ] + }, + "public.enum__events_v_version_main_cta_target": { + "name": "enum__events_v_version_main_cta_target", + "schema": "public", + "values": [ + "_self", + "_blank" + ] + }, + "public.enum__events_v_version_status": { + "name": "enum__events_v_version_status", + "schema": "public", + "values": [ + "draft", + "published" + ] + }, + "public.enum_posts_launchweek": { + "name": "enum_posts_launchweek", + "schema": "public", + "values": [ + "6", + "7", + "8", + "x", + "ga", + "12", + "13", + "14", + "15" + ] + }, + "public.enum_posts_status": { + "name": "enum_posts_status", + "schema": "public", + "values": [ + "draft", + "published" + ] + }, + "public.enum__posts_v_version_launchweek": { + "name": "enum__posts_v_version_launchweek", + "schema": "public", + "values": [ + "6", + "7", + "8", + "x", + "ga", + "12", + "13", + "14", + "15" + ] + }, + "public.enum__posts_v_version_status": { + "name": "enum__posts_v_version_status", + "schema": "public", + "values": [ + "draft", + "published" + ] + }, + "public.enum_users_roles": { + "name": "enum_users_roles", + "schema": "public", + "values": [ + "admin", + "editor" + ] + }, + "public.enum_payload_jobs_log_task_slug": { + "name": "enum_payload_jobs_log_task_slug", + "schema": "public", + "values": [ + "inline", + "schedulePublish" + ] + }, + "public.enum_payload_jobs_log_state": { + "name": "enum_payload_jobs_log_state", + "schema": "public", + "values": [ + "failed", + "succeeded" + ] + }, + "public.enum_payload_jobs_task_slug": { + "name": "enum_payload_jobs_task_slug", + "schema": "public", + "values": [ + "inline", + "schedulePublish" + ] + } + }, + "schemas": {}, + "sequences": {}, + "roles": {}, + "policies": {}, + "views": {}, + "_meta": { + "schemas": {}, + "tables": {}, + "columns": {} + } +} \ No newline at end of file diff --git a/apps/cms/src/migrations/20250808_145722_update_payload_migrations.ts b/apps/cms/src/migrations/20250808_145722_update_payload_migrations.ts new file mode 100644 index 00000000000..925b8cdb552 --- /dev/null +++ b/apps/cms/src/migrations/20250808_145722_update_payload_migrations.ts @@ -0,0 +1,21 @@ +import { MigrateUpArgs, MigrateDownArgs, sql } from '@payloadcms/db-postgres' + +export async function up({ db }: MigrateUpArgs): Promise { + await db.execute(sql` + CREATE TABLE "users_sessions" ( + "_order" integer NOT NULL, + "_parent_id" integer NOT NULL, + "id" varchar PRIMARY KEY NOT NULL, + "created_at" timestamp(3) with time zone, + "expires_at" timestamp(3) with time zone NOT NULL + ); + + ALTER TABLE "users_sessions" ADD CONSTRAINT "users_sessions_parent_id_fk" FOREIGN KEY ("_parent_id") REFERENCES "public"."users"("id") ON DELETE cascade ON UPDATE no action; + CREATE INDEX "users_sessions_order_idx" ON "users_sessions" USING btree ("_order"); + CREATE INDEX "users_sessions_parent_id_idx" ON "users_sessions" USING btree ("_parent_id");`) +} + +export async function down({ db, payload, req }: MigrateDownArgs): Promise { + await db.execute(sql` + DROP TABLE "users_sessions" CASCADE;`) +} diff --git a/apps/cms/src/migrations/index.ts b/apps/cms/src/migrations/index.ts index 9137c23dbd9..142575f5588 100644 --- a/apps/cms/src/migrations/index.ts +++ b/apps/cms/src/migrations/index.ts @@ -1,9 +1,21 @@ -import * as migration_20250529_103319 from './20250529_103319'; +import * as migration_20250619_155345 from './20250619_155345.ts'; +import * as migration_20250619_160549 from './20250619_160549.ts'; +import * as migration_20250808_145722_update_payload_migrations from './20250808_145722_update_payload_migrations.ts'; export const migrations = [ { - up: migration_20250529_103319.up, - down: migration_20250529_103319.down, - name: '20250529_103319' + up: migration_20250619_155345.up, + down: migration_20250619_155345.down, + name: '20250619_155345', + }, + { + up: migration_20250619_160549.up, + down: migration_20250619_160549.down, + name: '20250619_160549', + }, + { + up: migration_20250808_145722_update_payload_migrations.up, + down: migration_20250808_145722_update_payload_migrations.down, + name: '20250808_145722_update_payload_migrations' }, ]; diff --git a/apps/cms/src/payload-types.ts b/apps/cms/src/payload-types.ts index 60b5563ffb4..5b1b3b9600a 100644 --- a/apps/cms/src/payload-types.ts +++ b/apps/cms/src/payload-types.ts @@ -987,6 +987,13 @@ export interface User { hash?: string | null; loginAttempts?: number | null; lockUntil?: string | null; + sessions?: + | { + id: string; + createdAt?: string | null; + expiresAt: string; + }[] + | null; password?: string | null; } /** @@ -1454,6 +1461,13 @@ export interface UsersSelect { hash?: T; loginAttempts?: T; lockUntil?: T; + sessions?: + | T + | { + id?: T; + createdAt?: T; + expiresAt?: T; + }; } /** * This interface was referenced by `Config`'s JSON-Schema diff --git a/apps/cms/src/payload.config.ts b/apps/cms/src/payload.config.ts index 007ad5d8803..6c715330bb1 100644 --- a/apps/cms/src/payload.config.ts +++ b/apps/cms/src/payload.config.ts @@ -1,23 +1,78 @@ -import { postgresAdapter } from '@payloadcms/db-postgres' -import path from 'path' -import { buildConfig } from 'payload' -import { fileURLToPath } from 'url' +import path from 'node:path' +import { fileURLToPath } from 'node:url' import sharp from 'sharp' -import { plugins } from './plugins' -import { defaultLexical } from '@/fields/defaultLexical' -import { getServerSideURL } from './utilities/getURL' +import { postgresAdapter } from '@payloadcms/db-postgres' +import { payloadCloudPlugin } from '@payloadcms/payload-cloud' +import { nestedDocsPlugin } from '@payloadcms/plugin-nested-docs' +import { seoPlugin } from '@payloadcms/plugin-seo' +import { s3Storage } from '@payloadcms/storage-s3' +import { buildConfig, type Plugin } from 'payload' +import { defaultLexical } from './fields/defaultLexical.ts' +import { getServerSideURL } from './utilities/getURL.ts' -import { Authors } from './collections/Authors' -import { Categories } from './collections/Categories' -import { Customers } from './collections/Customers' -import { Events } from './collections/Events' -import { Media } from './collections/Media' -import { Posts } from './collections/Posts' -import { Tags } from './collections/Tags' -import { Users } from './collections/Users' +import type { GenerateTitle, GenerateURL } from '@payloadcms/plugin-seo/types' +import type { Customer, Event, Post } from './payload-types' + +import { Authors } from './collections/Authors.ts' +import { Categories } from './collections/Categories.ts' +import { Customers } from './collections/Customers/index.ts' +import { Events } from './collections/Events/index.ts' +import { Media } from './collections/Media.ts' +import { Posts } from './collections/Posts/index.ts' +import { Tags } from './collections/Tags.ts' +import { Users } from './collections/Users.ts' const filename = fileURLToPath(import.meta.url) const dirname = path.dirname(filename) +const siteName = 'Supabase' + +const generateTitle: GenerateTitle = ({ doc, collectionSlug }: any) => { + switch (collectionSlug) { + case 'customers': + return `${doc.name} | ${siteName} Customer Stories` + case 'events': + return `${doc.title} | ${siteName} Events` + case 'posts': + return doc.title + default: + return `${doc.title} | ${siteName}` + } +} + +const generateURL: GenerateURL = ({ doc }: any) => { + const url = getServerSideURL() + + return doc?.slug ? `${url}/${doc.slug}` : url +} + +const plugins: Plugin[] = [ + nestedDocsPlugin({ + collections: ['categories'], + generateURL: (docs) => docs.reduce((url, doc) => `${url}/${doc.slug}`, ''), + }), + seoPlugin({ + generateTitle, + generateURL, + }), + payloadCloudPlugin(), + s3Storage({ + collections: { + media: { + prefix: 'media', + }, + }, + bucket: process.env.S3_BUCKET || '', + config: { + forcePathStyle: true, + credentials: { + accessKeyId: process.env.S3_ACCESS_KEY_ID || '', + secretAccessKey: process.env.S3_SECRET_ACCESS_KEY || '', + }, + region: process.env.S3_REGION, + endpoint: process.env.S3_ENDPOINT, + }, + }), +] export default buildConfig({ admin: { @@ -59,7 +114,7 @@ export default buildConfig({ pool: { connectionString: process.env.DATABASE_URI || '', }, - schemaName: 'cms-payload', + // schemaName: 'cms-payload', }), sharp, plugins, diff --git a/apps/cms/src/payload.migrate.config.ts b/apps/cms/src/payload.migrate.config.ts new file mode 100644 index 00000000000..6a37bfc6a8a --- /dev/null +++ b/apps/cms/src/payload.migrate.config.ts @@ -0,0 +1,11 @@ +import { postgresAdapter } from '@payloadcms/db-postgres' +import { buildConfig } from 'payload' + +export default buildConfig({ + secret: process.env.PAYLOAD_SECRET, + db: postgresAdapter({ + pool: { + connectionString: process.env.DATABASE_URI || '', + }, + }), +}) diff --git a/apps/cms/src/plugins/index.ts b/apps/cms/src/plugins/index.ts deleted file mode 100644 index 7d6390c8994..00000000000 --- a/apps/cms/src/plugins/index.ts +++ /dev/null @@ -1,87 +0,0 @@ -import { payloadCloudPlugin } from '@payloadcms/payload-cloud' -// import { formBuilderPlugin } from '@payloadcms/plugin-form-builder' -import { nestedDocsPlugin } from '@payloadcms/plugin-nested-docs' -import { seoPlugin } from '@payloadcms/plugin-seo' -import { s3Storage } from '@payloadcms/storage-s3' -import { Plugin } from 'payload' -import { GenerateTitle, GenerateURL } from '@payloadcms/plugin-seo/types' -// import { FixedToolbarFeature, HeadingFeature, lexicalEditor } from '@payloadcms/richtext-lexical' - -import { Customer, Event, Post } from '@/payload-types' -import { getServerSideURL } from '@/utilities/getURL' - -const siteName = 'Supabase' - -const generateTitle: GenerateTitle = ({ doc, collectionSlug }) => { - switch (collectionSlug) { - case 'customers': - return `${doc.name} | ${siteName} Customer Stories` - case 'events': - return `${doc.title} | ${siteName} Events` - case 'posts': - return doc.title - default: - return `${doc.title} | ${siteName}` - } -} - -const generateURL: GenerateURL = ({ doc }) => { - const url = getServerSideURL() - - return doc?.slug ? `${url}/${doc.slug}` : url -} - -export const plugins: Plugin[] = [ - nestedDocsPlugin({ - collections: ['categories'], - generateURL: (docs) => docs.reduce((url, doc) => `${url}/${doc.slug}`, ''), - }), - seoPlugin({ - generateTitle, - generateURL, - }), - // formBuilderPlugin({ - // fields: { - // payment: false, - // }, - // formOverrides: { - // fields: ({ defaultFields }) => { - // return defaultFields.map((field) => { - // if ('name' in field && field.name === 'confirmationMessage') { - // return { - // ...field, - // editor: lexicalEditor({ - // features: ({ rootFeatures }) => { - // return [ - // ...rootFeatures, - // FixedToolbarFeature(), - // HeadingFeature({ enabledHeadingSizes: ['h1', 'h2', 'h3', 'h4'] }), - // ] - // }, - // }), - // } - // } - // return field - // }) - // }, - // }, - // }), - payloadCloudPlugin(), - s3Storage({ - collections: { - media: { - prefix: 'media', - }, - }, - bucket: process.env.S3_BUCKET || '', - config: { - forcePathStyle: true, - credentials: { - accessKeyId: process.env.S3_ACCESS_KEY_ID || '', - secretAccessKey: process.env.S3_SECRET_ACCESS_KEY || '', - }, - region: process.env.S3_REGION, - endpoint: process.env.S3_ENDPOINT, - }, - }), -] diff --git a/apps/cms/src/providers/HeaderTheme/index.tsx b/apps/cms/src/providers/HeaderTheme/index.tsx index e006c21c2e9..bf88fc5a71a 100644 --- a/apps/cms/src/providers/HeaderTheme/index.tsx +++ b/apps/cms/src/providers/HeaderTheme/index.tsx @@ -27,7 +27,11 @@ export const HeaderThemeProvider = ({ children }: { children: React.ReactNode }) setThemeState(themeToSet) }, []) - return {children} + return ( + + {children} + + ) } export const useHeaderTheme = (): ContextType => use(HeaderThemeContext) diff --git a/apps/cms/src/providers/Theme/index.tsx b/apps/cms/src/providers/Theme/index.tsx index 66e2f58635a..060abbac766 100644 --- a/apps/cms/src/providers/Theme/index.tsx +++ b/apps/cms/src/providers/Theme/index.tsx @@ -51,7 +51,7 @@ export const ThemeProvider = ({ children }: { children: React.ReactNode }) => { setThemeState(themeToSet) }, []) - return {children} + return {children} } export const useTheme = (): ThemeContextType => use(ThemeContext) diff --git a/apps/cms/src/providers/Theme/shared.ts b/apps/cms/src/providers/Theme/shared.ts index a038908183a..4d0b0042c2b 100644 --- a/apps/cms/src/providers/Theme/shared.ts +++ b/apps/cms/src/providers/Theme/shared.ts @@ -1,4 +1,4 @@ -import type { Theme } from './types' +import type { Theme } from './types.ts' export const themeLocalStorageKey = 'payload-theme' diff --git a/apps/cms/src/search/Component.tsx b/apps/cms/src/search/Component.tsx deleted file mode 100644 index 6c26847ff41..00000000000 --- a/apps/cms/src/search/Component.tsx +++ /dev/null @@ -1,41 +0,0 @@ -'use client' -import { Input } from '@/components/ui/input' -import { Label } from '@/components/ui/label' -import React, { useState, useEffect } from 'react' -import { useDebounce } from '@/utilities/useDebounce' -import { useRouter } from 'next/navigation' - -export const Search: React.FC = () => { - const [value, setValue] = useState('') - const router = useRouter() - - const debouncedValue = useDebounce(value) - - useEffect(() => { - router.push(`/search${debouncedValue ? `?q=${debouncedValue}` : ''}`) - }, [debouncedValue, router]) - - return ( -
-
{ - e.preventDefault() - }} - > - - { - setValue(event.target.value) - }} - placeholder="Search" - /> - -
-
- ) -} diff --git a/apps/cms/src/search/beforeSync.ts b/apps/cms/src/search/beforeSync.ts deleted file mode 100644 index aa421593abf..00000000000 --- a/apps/cms/src/search/beforeSync.ts +++ /dev/null @@ -1,44 +0,0 @@ -import { BeforeSync, DocToSync } from '@payloadcms/plugin-search/types' - -export const beforeSyncWithSearch: BeforeSync = async ({ originalDoc, searchDoc }) => { - const { - doc: { relationTo: collection }, - } = searchDoc - - const { slug, id, categories, title, meta } = originalDoc - - const modifiedDoc: DocToSync = { - ...searchDoc, - slug, - meta: { - ...meta, - title: meta?.title || title, - image: meta?.image?.id || meta?.image, - description: meta?.description, - }, - categories: [], - } - - if (categories && Array.isArray(categories) && categories.length > 0) { - // get full categories and keep a flattened copy of their most important properties - try { - const mappedCategories = categories.map((category) => { - const { id, title } = category - - return { - relationTo: 'categories', - id, - title, - } - }) - - modifiedDoc.categories = mappedCategories - } catch (_err) { - console.error( - `Failed. Category not found when syncing collection '${collection}' with id: '${id}' to search.` - ) - } - } - - return modifiedDoc -} diff --git a/apps/cms/src/search/fieldOverrides.ts b/apps/cms/src/search/fieldOverrides.ts deleted file mode 100644 index 6d086cb95f1..00000000000 --- a/apps/cms/src/search/fieldOverrides.ts +++ /dev/null @@ -1,61 +0,0 @@ -import { Field } from 'payload' - -export const searchFields: Field[] = [ - { - name: 'slug', - type: 'text', - index: true, - admin: { - readOnly: true, - }, - }, - { - name: 'meta', - label: 'Meta', - type: 'group', - index: true, - admin: { - readOnly: true, - }, - fields: [ - { - type: 'text', - name: 'title', - label: 'Title', - }, - { - type: 'text', - name: 'description', - label: 'Description', - }, - { - name: 'image', - label: 'Image', - type: 'upload', - relationTo: 'media', - }, - ], - }, - { - label: 'Categories', - name: 'categories', - type: 'array', - admin: { - readOnly: true, - }, - fields: [ - { - name: 'relationTo', - type: 'text', - }, - { - name: 'id', - type: 'text', - }, - { - name: 'title', - type: 'text', - }, - ], - }, -] diff --git a/apps/cms/src/utilities/formatAuthors.ts b/apps/cms/src/utilities/formatAuthors.ts deleted file mode 100644 index 89b92a18a2c..00000000000 --- a/apps/cms/src/utilities/formatAuthors.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { Post } from '@/payload-types' - -/** - * Formats an array of populatedAuthors from Posts into a prettified string. - * @param authors - The populatedAuthors array from a Post. - * @returns A prettified string of authors. - * @example - * - * [Author1, Author2] becomes 'Author1 and Author2' - * [Author1, Author2, Author3] becomes 'Author1, Author2, and Author3' - * - */ -export const formatAuthors = ( - authors: NonNullable[number]>[] -) => { - // Ensure we don't have any authors without a name - const authorNames = authors.map((author) => author.name).filter(Boolean) - - if (authorNames.length === 0) return '' - if (authorNames.length === 1) return authorNames[0] - if (authorNames.length === 2) return `${authorNames[0]} and ${authorNames[1]}` - - return `${authorNames.slice(0, -1).join(', ')} and ${authorNames[authorNames.length - 1]}` -} diff --git a/apps/cms/src/utilities/generateMeta.ts b/apps/cms/src/utilities/generateMeta.ts index f5b571d82bb..bde0b59da52 100644 --- a/apps/cms/src/utilities/generateMeta.ts +++ b/apps/cms/src/utilities/generateMeta.ts @@ -2,8 +2,8 @@ import type { Metadata } from 'next' import type { Media, Post, Config } from '../payload-types' -import { mergeOpenGraph } from './mergeOpenGraph' -import { getServerSideURL } from './getURL' +import { mergeOpenGraph } from './mergeOpenGraph.ts' +import { getServerSideURL } from './getURL.ts' const getImageURL = (image?: Media | Config['db']['defaultIDType'] | null) => { const serverUrl = getServerSideURL() diff --git a/apps/cms/src/utilities/generatePreviewPath.ts b/apps/cms/src/utilities/generatePreviewPath.ts index 6a2322df007..230edef6508 100644 --- a/apps/cms/src/utilities/generatePreviewPath.ts +++ b/apps/cms/src/utilities/generatePreviewPath.ts @@ -2,7 +2,6 @@ import { PayloadRequest, CollectionSlug } from 'payload' const collectionPrefixMap: Partial> = { posts: '/blog', - pages: '', } type Props = { diff --git a/apps/cms/src/utilities/getDocument.ts b/apps/cms/src/utilities/getDocument.ts index 0dcb5ed4273..31e79ed4ead 100644 --- a/apps/cms/src/utilities/getDocument.ts +++ b/apps/cms/src/utilities/getDocument.ts @@ -2,7 +2,7 @@ import type { Config } from 'src/payload-types' import configPromise from '@payload-config' import { getPayload } from 'payload' -import { unstable_cache } from 'next/cache' +// Do not import next/cache at module scope to keep migrate step Node-compatible type Collection = keyof Config['collections'] @@ -25,7 +25,16 @@ async function getDocument(collection: Collection, slug: string, depth = 0) { /** * Returns a unstable_cache function mapped with the cache tag for the slug */ -export const getCachedDocument = (collection: Collection, slug: string) => - unstable_cache(async () => getDocument(collection, slug), [collection, slug], { - tags: [`${collection}_${slug}`], - }) +export const getCachedDocument = (collection: Collection, slug: string) => { + return async () => { + try { + const { unstable_cache } = await import('next/cache') + const cached = unstable_cache(async () => getDocument(collection, slug), [collection, slug], { + tags: [`${collection}_${slug}`], + }) + return cached() + } catch { + return getDocument(collection, slug) + } + } +} diff --git a/apps/cms/src/utilities/getGlobals.ts b/apps/cms/src/utilities/getGlobals.ts index d37a2aa03c8..39d6830b195 100644 --- a/apps/cms/src/utilities/getGlobals.ts +++ b/apps/cms/src/utilities/getGlobals.ts @@ -2,7 +2,7 @@ import type { Config } from 'src/payload-types' import configPromise from '@payload-config' import { getPayload } from 'payload' -import { unstable_cache } from 'next/cache' +// Do not import next/cache at module scope to keep migrate step Node-compatible type Global = keyof Config['globals'] @@ -20,7 +20,16 @@ async function getGlobal(slug: Global, depth = 0) { /** * Returns a unstable_cache function mapped with the cache tag for the slug */ -export const getCachedGlobal = (slug: Global, depth = 0) => - unstable_cache(async () => getGlobal(slug, depth), [slug], { - tags: [`global_${slug}`], - }) +export const getCachedGlobal = (slug: Global, depth = 0) => { + return async () => { + try { + const { unstable_cache } = await import('next/cache') + const cached = unstable_cache(async () => getGlobal(slug, depth), [slug], { + tags: [`global_${slug}`], + }) + return cached() + } catch { + return getGlobal(slug, depth) + } + } +} diff --git a/apps/cms/src/utilities/getMeUser.ts b/apps/cms/src/utilities/getMeUser.ts index ad9e5c1dfe7..6ce8100c794 100644 --- a/apps/cms/src/utilities/getMeUser.ts +++ b/apps/cms/src/utilities/getMeUser.ts @@ -2,7 +2,7 @@ import { cookies } from 'next/headers' import { redirect } from 'next/navigation' import type { User } from '../payload-types' -import { getClientSideURL } from './getURL' +import { getClientSideURL } from './getURL.ts' export const getMeUser = async (args?: { nullUserRedirect?: string diff --git a/apps/cms/src/utilities/getRedirects.ts b/apps/cms/src/utilities/getRedirects.ts deleted file mode 100644 index 8eac7f2cf5b..00000000000 --- a/apps/cms/src/utilities/getRedirects.ts +++ /dev/null @@ -1,26 +0,0 @@ -import configPromise from '@payload-config' -import { getPayload } from 'payload' -import { unstable_cache } from 'next/cache' - -export async function getRedirects(depth = 1) { - const payload = await getPayload({ config: configPromise }) - - const { docs: redirects } = await payload.find({ - collection: 'redirects', - depth, - limit: 0, - pagination: false, - }) - - return redirects -} - -/** - * Returns a unstable_cache function mapped with the cache tag for 'redirects'. - * - * Cache all redirects together to avoid multiple fetches. - */ -export const getCachedRedirects = () => - unstable_cache(async () => getRedirects(), ['redirects'], { - tags: ['redirects'], - }) diff --git a/apps/cms/src/utilities/getURL.ts b/apps/cms/src/utilities/getURL.ts index c05c13ee789..e7622965ac8 100644 --- a/apps/cms/src/utilities/getURL.ts +++ b/apps/cms/src/utilities/getURL.ts @@ -1,4 +1,4 @@ -import canUseDOM from './canUseDOM' +import canUseDOM from './canUseDOM.ts' export const getServerSideURL = () => { let url = process.env.NEXT_PUBLIC_SERVER_URL @@ -8,7 +8,7 @@ export const getServerSideURL = () => { } if (!url) { - url = 'http://localhost:3000' + url = 'http://localhost:3030' } return url diff --git a/apps/cms/src/utilities/mergeOpenGraph.ts b/apps/cms/src/utilities/mergeOpenGraph.ts index a331a2a15b6..0ec43bb8d1b 100644 --- a/apps/cms/src/utilities/mergeOpenGraph.ts +++ b/apps/cms/src/utilities/mergeOpenGraph.ts @@ -1,5 +1,5 @@ import type { Metadata } from 'next' -import { getServerSideURL } from './getURL' +import { getServerSideURL } from './getURL.ts' const defaultOpenGraph: Metadata['openGraph'] = { type: 'website', diff --git a/apps/cms/supabase/config.toml b/apps/cms/supabase/config.toml index 255373749f1..f12262c6548 100644 --- a/apps/cms/supabase/config.toml +++ b/apps/cms/supabase/config.toml @@ -2,12 +2,12 @@ # https://supabase.com/docs/guides/local-development/cli/config # A string used to distinguish different Supabase projects on the same host. Defaults to the # working directory name when running `supabase init`. -project_id = "cms-payload" +project_id = "cms" [api] enabled = true # Port to use for the API URL. -port = 54321 +port = 34321 # Schemas to expose in your API. Tables, views and stored procedures in this schema will get API # endpoints. `public` and `graphql_public` schemas are included by default. schemas = ["public", "graphql_public"] @@ -23,17 +23,17 @@ enabled = false [db] # Port to use for the local database URL. -port = 54322 +port = 34322 # Port used by db diff command to initialize the shadow database. -shadow_port = 54320 +shadow_port = 34320 # The database major version to use. This has to be the same as your remote database's. Run `SHOW # server_version;` on the remote database to check. -major_version = 15 +major_version = 17 [db.pooler] enabled = false # Port to use for the local connection pooler. -port = 54329 +port = 34329 # Specifies when a server connection can be reused by other clients. # Configure one of the supported pooler modes: `transaction`, `session`. pool_mode = "transaction" @@ -46,6 +46,8 @@ max_client_conn = 100 # secret_key = "env(SECRET_VALUE)" [db.migrations] +# If disabled, migrations will be skipped during a db push or reset. +enabled = true # Specifies an ordered list of schema files that describe your database. # Supports glob patterns relative to supabase directory: "./schemas/*.sql" schema_paths = [] @@ -57,6 +59,16 @@ enabled = true # Supports glob patterns relative to supabase directory: "./seeds/*.sql" sql_paths = ["./seed.sql"] +[db.network_restrictions] +# Enable management of network restrictions. +enabled = false +# List of IPv4 CIDR blocks allowed to connect to the database. +# Defaults to allow all IPv4 connections. Set empty array to block all IPs. +allowed_cidrs = ["0.0.0.0/0"] +# List of IPv6 CIDR blocks allowed to connect to the database. +# Defaults to allow all IPv6 connections. Set empty array to block all IPs. +allowed_cidrs_v6 = ["::/0"] + [realtime] enabled = true # Bind realtime via either IPv4 or IPv6. (default: IPv4) @@ -67,7 +79,7 @@ enabled = true [studio] enabled = true # Port to use for Supabase Studio. -port = 54323 +port = 34323 # External URL of the API server that frontend connects to. api_url = "http://127.0.0.1" # OpenAI API Key to use for Supabase AI in the Supabase Studio. @@ -78,10 +90,10 @@ openai_api_key = "env(OPENAI_API_KEY)" [inbucket] enabled = true # Port to use for the email testing server web interface. -port = 54324 +port = 34324 # Uncomment to expose additional ports for testing user applications that send emails. -# smtp_port = 54325 -# pop3_port = 54326 +# smtp_port = 34325 +# pop3_port = 34326 # admin_email = "admin@email.com" # sender_name = "Admin" @@ -110,6 +122,8 @@ site_url = "http://127.0.0.1:3000" additional_redirect_urls = ["https://127.0.0.1:3000"] # How long tokens are valid for, in seconds. Defaults to 3600 (1 hour), maximum 604,800 (1 week). jwt_expiry = 3600 +# Path to JWT signing key. DO NOT commit your signing keys file to git. +# signing_keys_path = "./signing_keys.json" # If disabled, the refresh token will never expire. enable_refresh_token_rotation = true # Allows refresh tokens to be reused after expiry, up to the specified interval in seconds. @@ -140,6 +154,8 @@ token_refresh = 150 sign_in_sign_ups = 30 # Number of OTP / Magic link verifications that can be made in a 5 minute interval per IP address. token_verifications = 30 +# Number of Web3 logins that can be made in a 5 minute interval per IP address. +web3 = 30 # Configure one of the supported captcha providers: `hcaptcha`, `turnstile`. # [auth.captcha] @@ -200,6 +216,11 @@ max_frequency = "5s" # Force log out if the user has been inactive longer than the specified duration. # inactivity_timeout = "8h" +# This hook runs before a new user is created and allows developers to reject the request based on the incoming user object. +# [auth.hook.before_user_created] +# enabled = true +# uri = "pg-functions://postgres/auth/before-user-created-hook" + # This hook runs before a token is issued and allows you to add additional claims based on the authentication method used. # [auth.hook.custom_access_token] # enabled = true @@ -252,6 +273,11 @@ url = "" # If enabled, the nonce check will be skipped. Required for local sign in with Google auth. skip_nonce_check = false +# Allow Solana wallet holders to sign in to your project via the Sign in with Solana (SIWS, EIP-4361) standard. +# You can configure "web3" rate limit in the [auth.rate_limit] section and set up [auth.captcha] if self-hosting. +[auth.web3.solana] +enabled = false + # Use Firebase Auth as a third-party provider alongside Supabase Auth. [auth.third_party.firebase] enabled = false @@ -276,7 +302,7 @@ enabled = false # domain = "example.clerk.accounts.dev" [edge_runtime] -enabled = true +enabled = false # Configure one of the supported request policies: `oneshot`, `per_worker`. # Use `oneshot` for hot reload, or `per_worker` for load testing. policy = "oneshot" @@ -289,8 +315,8 @@ deno_version = 1 # secret_key = "env(SECRET_VALUE)" [analytics] -enabled = true -port = 54327 +enabled = false +port = 34327 # Configure one of the supported backends: `postgres`, `bigquery`. backend = "postgres" diff --git a/apps/cms/tsconfig.json b/apps/cms/tsconfig.json index 2943d23d22c..5a66dbfae8e 100644 --- a/apps/cms/tsconfig.json +++ b/apps/cms/tsconfig.json @@ -1,10 +1,8 @@ { "compilerOptions": { - /* Strictness */ "strict": true, "noUncheckedIndexedAccess": true, "noImplicitOverride": true, - "baseUrl": ".", "esModuleInterop": true, "target": "ES2022", @@ -18,6 +16,7 @@ "jsx": "preserve", "module": "esnext", "moduleResolution": "bundler", + "allowImportingTsExtensions": true, "resolveJsonModule": true, "sourceMap": true, "isolatedModules": true, diff --git a/apps/cms/vercel.json b/apps/cms/vercel.json new file mode 100644 index 00000000000..fd0cbd73ac1 --- /dev/null +++ b/apps/cms/vercel.json @@ -0,0 +1,5 @@ +{ + "$schema": "https://openapi.vercel.sh/vercel.json", + "buildCommand": "pnpm vercel-build", + "installCommand": "pnpm install" +} diff --git a/apps/docs/app/api/graphql/tests/searchDocs.test.ts b/apps/docs/app/api/graphql/tests/searchDocs.test.ts index 2f1f9524a92..92b9012a862 100644 --- a/apps/docs/app/api/graphql/tests/searchDocs.test.ts +++ b/apps/docs/app/api/graphql/tests/searchDocs.test.ts @@ -4,7 +4,9 @@ import { type OpenAIClientInterface } from '~/lib/openAi' import { ApiError } from '../../utils' import { POST } from '../route' -const contentEmbeddingMock = vi.fn().mockImplementation(async () => Result.ok([0.1, 0.2, 0.3])) +const contentEmbeddingMock = vi + .fn() + .mockImplementation(async () => Result.ok({ embedding: [0.1, 0.2, 0.3], tokenCount: 10 })) const openAIMock: OpenAIClientInterface = { createContentEmbedding: contentEmbeddingMock, } diff --git a/apps/docs/app/api/utils.ts b/apps/docs/app/api/utils.ts index 652ea444e59..ffdfc4f4985 100644 --- a/apps/docs/app/api/utils.ts +++ b/apps/docs/app/api/utils.ts @@ -64,6 +64,16 @@ export class NoDataError
extends ApiError } } +export class FileNotFoundError
extends Error { + constructor( + message: string, + error: Error, + public details?: Details + ) { + super(`FileNotFound: ${message}`, { cause: error }) + } +} + export class MultiError extends Error { constructor( message: string, @@ -79,7 +89,7 @@ export class MultiError).push(error) + ;((this.cause ??= []) as Array).push(error) return this } } diff --git a/apps/docs/content/guides/auth/server-side/nextjs.mdx b/apps/docs/content/guides/auth/server-side/nextjs.mdx index 2cae8e90d46..0443da17257 100644 --- a/apps/docs/content/guides/auth/server-side/nextjs.mdx +++ b/apps/docs/content/guides/auth/server-side/nextjs.mdx @@ -264,7 +264,8 @@ export async function updateSession(request: NextRequest) { if ( !user && !request.nextUrl.pathname.startsWith('/login') && - !request.nextUrl.pathname.startsWith('/auth') + !request.nextUrl.pathname.startsWith('/auth') && + !request.nextUrl.pathname.startsWith('/error') ) { // no user, potentially respond by redirecting the user to the login page const url = request.nextUrl.clone() diff --git a/apps/docs/content/guides/auth/social-login/auth-google.mdx b/apps/docs/content/guides/auth/social-login/auth-google.mdx index fcacd776d7e..85bd5813d81 100644 --- a/apps/docs/content/guides/auth/social-login/auth-google.mdx +++ b/apps/docs/content/guides/auth/social-login/auth-google.mdx @@ -397,7 +397,7 @@ const OneTapComponent = () => { const supabase = createClient() const router = useRouter() - const initializeGoogleOneTap = () => { + const initializeGoogleOneTap = async () => { console.log('Initializing Google One Tap') const [nonce, hashedNonce] = await generateNonce() console.log('Nonce: ', nonce, hashedNonce) diff --git a/apps/docs/content/guides/auth/social-login/auth-linkedin.mdx b/apps/docs/content/guides/auth/social-login/auth-linkedin.mdx index c01d491f023..251b54e22ba 100644 --- a/apps/docs/content/guides/auth/social-login/auth-linkedin.mdx +++ b/apps/docs/content/guides/auth/social-login/auth-linkedin.mdx @@ -188,4 +188,4 @@ Do reach out to support if you have any concerns around this change. - [Supabase - Get started for free](https://supabase.com) - [Supabase JS Client](https://github.com/supabase/supabase-js) -- [LinkedIn Developer Dashboard](https://api.LinkedIn.com/apps) +- [LinkedIn Developer Dashboard](https://www.linkedin.com/developers/apps) diff --git a/apps/docs/content/guides/database/extensions/wrappers/overview.mdx b/apps/docs/content/guides/database/extensions/wrappers/overview.mdx index 210d86d9bce..a524a0d16b7 100644 --- a/apps/docs/content/guides/database/extensions/wrappers/overview.mdx +++ b/apps/docs/content/guides/database/extensions/wrappers/overview.mdx @@ -92,7 +92,7 @@ This approach has several benefits: 1. **On-demand:** analytical data is immediately available within your application with no additional infrastructure. 1. **Always in sync:** since the data is queried directly from the remote server, it's always up-to-date. 1. **Integrated:** large datasets are available within your application, and can be joined with your operational/transactional data. -1. **Save on bandwidth:** only extract/load what you need. +1. **Save on egress:** only extract/load what you need. ### Batch ETL with Wrappers diff --git a/apps/docs/content/guides/database/postgres/row-level-security.mdx b/apps/docs/content/guides/database/postgres/row-level-security.mdx index 93e4ccf31e0..68056789cea 100644 --- a/apps/docs/content/guides/database/postgres/row-level-security.mdx +++ b/apps/docs/content/guides/database/postgres/row-level-security.mdx @@ -425,7 +425,7 @@ $$; create policy "rls_test_select" on test_table to authenticated -using ( private.has_good_role() ); +using ( (select private.has_good_role()) ); ``` diff --git a/apps/docs/content/guides/deployment/branching/configuration.mdx b/apps/docs/content/guides/deployment/branching/configuration.mdx index 5355264fff2..2c2d00a94ae 100644 --- a/apps/docs/content/guides/deployment/branching/configuration.mdx +++ b/apps/docs/content/guides/deployment/branching/configuration.mdx @@ -34,6 +34,19 @@ supabase --experimental branches create --persistent # Do you want to create a branch named develop? [Y/n] ``` + + +To retrieve the project ID for an existing branch, use the `branches list` command: + +```bash +supabase --experimental branches list +``` + +This will display a table showing all your branches with their corresponding project ID. +Use the value from the `BRANCH PROJECT ID` column as your `project_id` in the remote configuration. + + + ### Configuration merging When merging a PR into a persistent branch, the Supabase integration: @@ -186,6 +199,19 @@ max_rows = 500 pool_size = 25 ``` + + +To retrieve the project ID for an existing branch, use the `branches list` command: + +```bash +supabase --experimental branches list +``` + +This will display a table showing all your branches with their corresponding project ID. +Use the value from the `BRANCH PROJECT ID` column as your `project_id` in the remote configuration. + + + ### Feature branch configuration For feature branches that need specific settings: @@ -200,6 +226,19 @@ client_id = "env(GOOGLE_CLIENT_ID)" secret = "env(GOOGLE_CLIENT_SECRET)" ``` + + +To retrieve the project ID for an existing branch, use the `branches list` command: + +```bash +supabase --experimental branches list +``` + +This will display a table showing all your branches with their corresponding project ID. +Use the value from the `BRANCH PROJECT ID` column as your `project_id` in the remote configuration. + + + ## Next steps - Explore [branching integrations](/docs/guides/deployment/branching/integrations) diff --git a/apps/docs/content/guides/deployment/branching/dashboard.mdx b/apps/docs/content/guides/deployment/branching/dashboard.mdx index 2d938165736..a7577014d5a 100644 --- a/apps/docs/content/guides/deployment/branching/dashboard.mdx +++ b/apps/docs/content/guides/deployment/branching/dashboard.mdx @@ -58,8 +58,6 @@ When reviewing a merge request you may see a notice at the top of the page askin There are a few limitations you should be aware of before deciding to use branching without git. - Custom roles created through the dashboard are not captured on branch creation -- Only public schema changes are supported right now -- Extensions are not included in the diff process - Branches can only be merged to main; merging between preview branches is not supported - If your branch is out of date, you can pull in latest changes from main, but keep in mind that all functions will be overwritten - Deleting functions must be done manually on main branch diff --git a/apps/docs/content/guides/functions/websockets.mdx b/apps/docs/content/guides/functions/websockets.mdx index b9741bb7c60..e4d15d7ae9d 100644 --- a/apps/docs/content/guides/functions/websockets.mdx +++ b/apps/docs/content/guides/functions/websockets.mdx @@ -32,7 +32,7 @@ Here are some basic examples of setting up WebSocket servers using Deno and Node Deno.serve((req) => { const upgrade = req.headers.get('upgrade') || '' - if (upgrade.toLowerCase() != 'WebSocket') { + if (upgrade.toLowerCase() != 'websocket') { return new Response("request isn't trying to upgrade to WebSocket.", { status: 400 }) } diff --git a/apps/docs/content/guides/getting-started/quickstarts/nextjs.mdx b/apps/docs/content/guides/getting-started/quickstarts/nextjs.mdx index a6633706faa..14690e27cc3 100644 --- a/apps/docs/content/guides/getting-started/quickstarts/nextjs.mdx +++ b/apps/docs/content/guides/getting-started/quickstarts/nextjs.mdx @@ -59,12 +59,12 @@ hideToc: true - - + + - Create a new file at `app/instruments/page.tsx` and populate with the following. + Create a new file at `utils/supabase/server.ts` and populate with the following. - This will select all the rows from the `instruments` table in Supabase and render them on the page. + This creates a Supabase client, using the credentials from the `env.local` file. @@ -72,17 +72,6 @@ hideToc: true <$CodeTabs> - ```ts name=app/instruments/page.tsx - import { createClient } from '@/utils/supabase/server'; - - export default async function Instruments() { - const supabase = await createClient(); - const { data: instruments } = await supabase.from("instruments").select(); - - return
{JSON.stringify(instruments, null, 2)}
- } - ``` - ```ts name=utils/supabase/server.ts import { createServerClient } from '@supabase/ssr' import { cookies } from 'next/headers' @@ -122,6 +111,36 @@ hideToc: true
+ + + Create a new file at `app/instruments/page.tsx` and populate with the following. + + This selects all the rows from the `instruments` table in Supabase and render them on the page. + + + + + + <$CodeTabs> + + ```ts name=app/instruments/page.tsx + import { createClient } from '@/utils/supabase/server'; + + export default async function Instruments() { + const supabase = await createClient(); + const { data: instruments } = await supabase.from("instruments").select(); + + return
{JSON.stringify(instruments, null, 2)}
+ } + ``` + + + +
+ +
+ + Run the development server, go to http://localhost:3000/instruments in a browser and you should see the list of instruments. diff --git a/apps/docs/content/guides/getting-started/quickstarts/sveltekit.mdx b/apps/docs/content/guides/getting-started/quickstarts/sveltekit.mdx index c2b1702ebe8..032663d43fb 100644 --- a/apps/docs/content/guides/getting-started/quickstarts/sveltekit.mdx +++ b/apps/docs/content/guides/getting-started/quickstarts/sveltekit.mdx @@ -66,8 +66,8 @@ hideToc: true <$CodeTabs> ```text name=.env - VITE_PUBLIC_SUPABASE_URL= - VITE_PUBLIC_SUPABASE_ANON_KEY= + PUBLIC_SUPABASE_URL= + PUBLIC_SUPABASE_ANON_KEY= ``` @@ -89,16 +89,16 @@ hideToc: true ```js name=src/lib/supabaseClient.js import { createClient } from '@supabase/supabase-js'; - import { VITE_PUBLIC_SUPABASE_URL, VITE_PUBLIC_SUPABASE_ANON_KEY } from '$env/static/public'; + import { PUBLIC_SUPABASE_URL, PUBLIC_SUPABASE_ANON_KEY } from '$env/static/public'; - export const supabase = createClient(VITE_PUBLIC_SUPABASE_URL, VITE_PUBLIC_SUPABASE_ANON_KEY) + export const supabase = createClient(PUBLIC_SUPABASE_URL, PUBLIC_SUPABASE_ANON_KEY) ``` ```ts name=src/lib/supabaseClient.ts import { createClient } from '@supabase/supabase-js'; - import { VITE_PUBLIC_SUPABASE_URL, VITE_PUBLIC_SUPABASE_ANON_KEY } from '$env/static/public'; + import { PUBLIC_SUPABASE_URL, PUBLIC_SUPABASE_ANON_KEY } from '$env/static/public'; - export const supabase = createClient(VITE_PUBLIC_SUPABASE_URL, VITE_PUBLIC_SUPABASE_ANON_KEY) + export const supabase = createClient(PUBLIC_SUPABASE_URL, PUBLIC_SUPABASE_ANON_KEY) ``` diff --git a/apps/docs/content/guides/getting-started/tutorials/with-sveltekit.mdx b/apps/docs/content/guides/getting-started/tutorials/with-sveltekit.mdx index 1b114f50a60..43def489bf8 100644 --- a/apps/docs/content/guides/getting-started/tutorials/with-sveltekit.mdx +++ b/apps/docs/content/guides/getting-started/tutorials/with-sveltekit.mdx @@ -17,14 +17,14 @@ If you get stuck while working through this guide, refer to the [full example on ## Building the app -Let's start building the Svelte app from scratch. +Start building the Svelte app from scratch. ### Initialize a Svelte app -We can use the [SvelteKit Skeleton Project](https://kit.svelte.dev/docs) to initialize an app called `supabase-sveltekit` (for this tutorial we will be using TypeScript): +Use the [SvelteKit Skeleton Project](https://svelte.dev/docs/kit) to initialize an app called `supabase-sveltekit` (for this tutorial, select "SvelteKit minimal" and use TypeScript): ```bash -npm create svelte@latest supabase-sveltekit +npx sv create supabase-sveltekit cd supabase-sveltekit npm install ``` @@ -35,8 +35,8 @@ Then install the Supabase client library: [supabase-js](https://github.com/supab npm install @supabase/supabase-js ``` -And finally we want to save the environment variables in a `.env`. -All we need are the `SUPABASE_URL` and the `SUPABASE_KEY` key that you copied [earlier](#get-the-api-keys). +And finally, save the environment variables in a `.env` file. +All you need are the `PUBLIC_SUPABASE_URL` and the `PUBLIC_SUPABASE_ANON_KEY` key that you copied [earlier](#get-the-api-keys). <$CodeTabs> @@ -47,108 +47,46 @@ PUBLIC_SUPABASE_ANON_KEY="YOUR_SUPABASE_KEY" -Optionally, add `src/styles.css` with the [CSS from the example](https://raw.githubusercontent.com/supabase/supabase/master/examples/user-management/sveltekit-user-management/src/styles.css). +### App styling (optional) + +An optional step is to update the CSS file `src/styles.css` to make the app look nice. +You can find the full contents of this file [in the example repository](https://raw.githubusercontent.com/supabase/supabase/master/examples/user-management/sveltekit-user-management/src/styles.css). ### Creating a Supabase client for SSR -The `ssr` package configures Supabase to use Cookies, which is required for server-side languages and frameworks. +The `ssr` package configures Supabase to use Cookies, which are required for server-side languages and frameworks. -Install the Supabase packages: +Install the SSR package: ```bash -npm install @supabase/ssr @supabase/supabase-js +npm install @supabase/ssr ``` -Creating a Supabase client with the `ssr` package automatically configures it to use Cookies. This means your user's session is available throughout the entire SvelteKit stack - page, layout, server, hooks. +Creating a Supabase client with the `ssr` package automatically configures it to use Cookies. This means the user's session is available throughout the entire SvelteKit stack - page, layout, server, and hooks. -Add the code below to your `src/hooks.server.ts` to initialize the client on the server: +Add the code below to a `src/hooks.server.ts` file to initialize the client on the server: <$CodeTabs> -```ts name=src/hooks.server.ts -// src/hooks.server.ts -import { PUBLIC_SUPABASE_URL, PUBLIC_SUPABASE_ANON_KEY } from '$env/static/public' -import { createServerClient } from '@supabase/ssr' -import type { Handle } from '@sveltejs/kit' - -export const handle: Handle = async ({ event, resolve }) => { - event.locals.supabase = createServerClient(PUBLIC_SUPABASE_URL, PUBLIC_SUPABASE_ANON_KEY, { - cookies: { - getAll: () => event.cookies.getAll(), - /** - * SvelteKit's cookies API requires `path` to be explicitly set in - * the cookie options. Setting `path` to `/` replicates previous/ - * standard behavior. - */ - setAll: (cookiesToSet) => { - cookiesToSet.forEach(({ name, value, options }) => { - event.cookies.set(name, value, { ...options, path: '/' }) - }) - }, - }, - }) - - /** - * Unlike `supabase.auth.getSession()`, which returns the session _without_ - * validating the JWT, this function also calls `getUser()` to validate the - * JWT before returning the session. - */ - event.locals.safeGetSession = async () => { - const { - data: { session }, - } = await event.locals.supabase.auth.getSession() - if (!session) { - return { session: null, user: null } - } - - const { - data: { user }, - error, - } = await event.locals.supabase.auth.getUser() - if (error) { - // JWT validation has failed - return { session: null, user: null } - } - - return { session, user } - } - - return resolve(event, { - filterSerializedResponseHeaders(name) { - return name === 'content-range' || name === 'x-supabase-api-version' - }, - }) -} -``` +<$CodeSample +path="/user-management/sveltekit-user-management/src/hooks.server.ts" +lines={[[1, -1]]} +meta="name=src/hooks.server.ts" +/> <$Partial path="get_session_warning.mdx" /> - -If you are using TypeScript the compiler might complain about `event.locals.supabase` and `event.locals.safeGetSession`, this can be fixed by updating your `src/app.d.ts` with the content below: +{/* TODO: Change when adding JS autoconversion */} +As this tutorial uses TypeScript the compiler complains about `event.locals.supabase` and `event.locals.safeGetSession`, you can fix this by updating the `src/app.d.ts` with the content below: <$CodeTabs> -```ts name=src/app.d.ts -// src/app.d.ts - -import { SupabaseClient, Session } from '@supabase/supabase-js' - -declare global { - namespace App { - interface Locals { - supabase: SupabaseClient - safeGetSession(): Promise<{ session: Session | null; user: User | null }> - } - interface PageData { - session: Session | null - user: User | null - } - // interface Error {} - // interface Platform {} - } -} -``` +<$CodeSample +path="/user-management/sveltekit-user-management/src/app.d.ts" +lines={[[1, -1]]} +meta="name=src/app.d.ts" +/> @@ -156,26 +94,17 @@ Create a new `src/routes/+layout.server.ts` file to handle the session on the se <$CodeTabs> -```ts name=src/routes/+layout.server.ts -// src/routes/+layout.server.ts -import type { LayoutServerLoad } from './$types' - -export const load: LayoutServerLoad = async ({ locals: { safeGetSession }, cookies }) => { - const { session, user } = await safeGetSession() - - return { - session, - user, - cookies: cookies.getAll(), - } -} -``` +<$CodeSample +path="/user-management/sveltekit-user-management/src/routes/+layout.server.ts" +lines={[[1, -1]]} +meta="name=src/routes/+layout.server.ts" +/> -Start your dev server (`npm run dev`) in order to generate the `./$types` files we are referencing in our project. +Start the dev server (`npm run dev`) to generate the `./$types` files we are referencing in our project. @@ -183,202 +112,49 @@ Create a new `src/routes/+layout.ts` file to handle the session and the `supabas <$CodeTabs> -```ts name=src/routes/+layout.ts -// src/routes/+layout.ts -import { createBrowserClient, createServerClient, isBrowser } from '@supabase/ssr' -import { PUBLIC_SUPABASE_ANON_KEY, PUBLIC_SUPABASE_URL } from '$env/static/public' -import type { LayoutLoad } from './$types' - -export const load: LayoutLoad = async ({ fetch, data, depends }) => { - depends('supabase:auth') - - const supabase = isBrowser() - ? createBrowserClient(PUBLIC_SUPABASE_URL, PUBLIC_SUPABASE_ANON_KEY, { - global: { - fetch, - }, - }) - : createServerClient(PUBLIC_SUPABASE_URL, PUBLIC_SUPABASE_ANON_KEY, { - global: { - fetch, - }, - cookies: { - getAll() { - return data.cookies - }, - }, - }) - - /** - * It's fine to use `getSession` here, because on the client, `getSession` is - * safe, and on the server, it reads `session` from the `LayoutData`, which - * safely checked the session using `safeGetSession`. - */ - const { - data: { session }, - } = await supabase.auth.getSession() - - return { supabase, session } -} -``` +<$CodeSample +path="/user-management/sveltekit-user-management/src/routes/+layout.ts" +lines={[[1, -1]]} +meta="name=src/routes/+layout.ts" +/> -Update your `src/routes/+layout.svelte`: +Create `src/routes/+layout.svelte`: <$CodeTabs> -```svelte name=src/routes/+layout.svelte - - - - - User Management - - -
- -
-``` +<$CodeSample +path="/user-management/sveltekit-user-management/src/routes/+layout.svelte" +lines={[[1, -1]]} +meta="name=src/routes/+layout.svelte" +/> ## Set up a login page -Create a magic link login/signup page for your application: +Create a magic link login/signup page for your application by updating the `routes/+page.svelte` file: <$CodeTabs> -```svelte name=src/routes/+page.svelte - - - - - User Management - - -
-
-

Supabase + SvelteKit

-

Sign in via magic link with your email below

- {#if form?.message !== undefined} -
- {form?.message} -
- {/if} -
- - -
- {#if form?.errors?.email} - - {form?.errors?.email} - - {/if} -
- -
-
-
-``` +<$CodeSample +path="/user-management/sveltekit-user-management/src/routes/+page.svelte" +lines={[[1, -1]]} +meta="name=src/routes/+page.svelte" +/> -Create a `src/routes/+page.server.ts` file that will handle our magic link form when submitted. +Create a `src/routes/+page.server.ts` file that handles the magic link form when submitted. <$CodeTabs> -```ts name=src/routes/+page.server.ts -// src/routes/+page.server.ts -import { fail, redirect } from '@sveltejs/kit' -import type { Actions, PageServerLoad } from './$types' - -export const load: PageServerLoad = async ({ url, locals: { safeGetSession } }) => { - const { session } = await safeGetSession() - - // if the user is already logged in return them to the account page - if (session) { - redirect(303, '/account') - } - - return { url: url.origin } -} - -export const actions: Actions = { - default: async (event) => { - const { - url, - request, - locals: { supabase }, - } = event - const formData = await request.formData() - const email = formData.get('email') as string - const validEmail = /^[\w-\.+]+@([\w-]+\.)+[\w-]{2,8}$/.test(email) - - if (!validEmail) { - return fail(400, { errors: { email: 'Please enter a valid email address' }, email }) - } - - const { error } = await supabase.auth.signInWithOtp({ email }) - - if (error) { - return fail(400, { - success: false, - email, - message: `There was an issue, Please contact support.`, - }) - } - - return { - success: true, - message: 'Please check your email for a magic link to log into the website.', - } - }, -} -``` +<$CodeSample +path="/user-management/sveltekit-user-management/src/routes/+page.server.ts" +lines={[[1, -1]]} +meta="name=src/routes/+page.server.ts" +/> @@ -388,239 +164,77 @@ Change the email template to support a server-side authentication flow. Before we proceed, let's change the email template to support sending a token hash: -- Go to the [Auth templates](/dashboard/project/_/auth/templates) page in your dashboard. -- Select `Confirm signup` template. +- Go to the [**Auth** > **Emails**](/dashboard/project/_/auth/templates) page in the project dashboard. +- Select the **Confirm signup** template. - Change `{{ .ConfirmationURL }}` to `{{ .SiteURL }}/auth/confirm?token_hash={{ .TokenHash }}&type=email`. -- Repeat the previous step for `Magic link` template. +- Repeat the previous step for **Magic link** template. -Did you know? You could also customize emails sent out to new users, including the email's looks, content, and query parameters. Check out the [settings of your project](/dashboard/project/_/auth/templates). +**Did you know?** You can also customize emails sent out to new users, including the email's looks, content, and query parameters. Check out the [settings of your project](/dashboard/project/_/auth/templates). ### Confirmation endpoint -As we are working in a server-side rendering (SSR) environment, it is necessary to create a server endpoint responsible for exchanging the `token_hash` for a session. +As this is a server-side rendering (SSR) environment, you need to create a server endpoint responsible for exchanging the `token_hash` for a session. -In the following code snippet, we perform the following steps: +The following code snippet performs the following steps: -- Retrieve the `token_hash` sent back from the Supabase Auth server using the `token_hash` query parameter. -- Exchange this `token_hash` for a session, which we store in storage (in this case, cookies). -- Finally, the user is redirected to the `account` page or the `error` page. +- Retrieves the `token_hash` sent back from the Supabase Auth server using the `token_hash` query parameter. +- Exchanges this `token_hash` for a session, which you store in storage (in this case, cookies). +- Finally, redirect the user to the `account` page or the `error` page. <$CodeTabs> -```ts name=src/routes/auth/confirm/+server.ts -// src/routes/auth/confirm/+server.ts -import type { EmailOtpType } from '@supabase/supabase-js' -import { redirect } from '@sveltejs/kit' - -import type { RequestHandler } from './$types' - -export const GET: RequestHandler = async ({ url, locals: { supabase } }) => { - const token_hash = url.searchParams.get('token_hash') - const type = url.searchParams.get('type') as EmailOtpType | null - const next = url.searchParams.get('next') ?? '/account' - - /** - * Clean up the redirect URL by deleting the Auth flow parameters. - * - * `next` is preserved for now, because it's needed in the error case. - */ - const redirectTo = new URL(url) - redirectTo.pathname = next - redirectTo.searchParams.delete('token_hash') - redirectTo.searchParams.delete('type') - - if (token_hash && type) { - const { error } = await supabase.auth.verifyOtp({ type, token_hash }) - if (!error) { - redirectTo.searchParams.delete('next') - redirect(303, redirectTo) - } - } - - redirectTo.pathname = '/auth/error' - redirect(303, redirectTo) -} -``` +<$CodeSample +path="/user-management/sveltekit-user-management/src/routes/auth/confirm/+server.ts" +lines={[[1, -1]]} +meta="name=src/routes/auth/confirm/+server.ts" +/> ### Authentication error page -If there is an error with confirming the token you will be redirect to this error page. +If there is an error with confirming the token, redirect the user to an error page. <$CodeTabs> - -```svelte name=src/routes/auth/error/+page.svelte -

Login error

-``` +<$CodeSample +path="/user-management/sveltekit-user-management/src/routes/auth/error/+page.svelte" +lines={[[1, -1]]} +meta="name=src/routes/auth/error/+page.svelte" +/> ### Account page -After a user is signed in, they need to be able to edit their profile details and manage their account. +After a user signs in, they need to be able to edit their profile details page. Create a new `src/routes/account/+page.svelte` file with the content below. <$CodeTabs> -```svelte name=src/routes/account/+page.svelte - - - -
-
-
- - -
- -
- - -
- -
- - -
- -
- - -
- -
- -
-
- -
-
- -
-
-
-``` +<$CodeSample +path="/user-management/sveltekit-user-management/src/routes/account/+page.svelte" +lines={[[1, 3],[6,12],[15,38],[47,-1]]} +meta="name=src/routes/account/+page.svelte" +/> -Now create the associated `src/routes/account/+page.server.ts` file that will handle loading our data from the server through the `load` function -and handle all our form actions through the `actions` object. +Now, create the associated `src/routes/account/+page.server.ts` file that handles loading data from the server through the `load` function +and handle all form actions through the `actions` object. -```ts name=src/routes/account/+page.server.ts -import { fail, redirect } from '@sveltejs/kit' -import type { Actions, PageServerLoad } from './$types' - -export const load: PageServerLoad = async ({ locals: { supabase, safeGetSession } }) => { - const { session } = await safeGetSession() - - if (!session) { - redirect(303, '/') - } - - const { data: profile } = await supabase - .from('profiles') - .select(`username, full_name, website, avatar_url`) - .eq('id', session.user.id) - .single() - - return { session, profile } -} - -export const actions: Actions = { - update: async ({ request, locals: { supabase, safeGetSession } }) => { - const formData = await request.formData() - const fullName = formData.get('fullName') as string - const username = formData.get('username') as string - const website = formData.get('website') as string - const avatarUrl = formData.get('avatarUrl') as string - - const { session } = await safeGetSession() - - const { error } = await supabase.from('profiles').upsert({ - id: session?.user.id, - full_name: fullName, - username, - website, - avatar_url: avatarUrl, - updated_at: new Date(), - }) - - if (error) { - return fail(500, { - fullName, - username, - website, - avatarUrl, - }) - } - - return { - fullName, - username, - website, - avatarUrl, - } - }, - signout: async ({ locals: { supabase, safeGetSession } }) => { - const { session } = await safeGetSession() - if (session) { - await supabase.auth.signOut() - redirect(303, '/') - } - }, -} -``` +<$CodeSample +path="/user-management/sveltekit-user-management/src/routes/+page.server.ts" +lines={[[1, -1]]} +meta="name=src/routes/+page.server.ts" +/> ### Launch! -Now that we have all the pages in place, run this in a terminal window: +With all the pages in place, run this command in a terminal: ```bash npm run dev @@ -636,144 +250,29 @@ Every Supabase project is configured with [Storage](/docs/guides/storage) for ma ### Create an upload widget -Let's create an avatar for the user so that they can upload a profile photo. We can start by creating a new component called `Avatar.svelte` in the `src/routes/account` directory: +Create an avatar for the user so that they can upload a profile photo. Start by creating a new component called `Avatar.svelte` in the `src/routes/account` directory: <$CodeTabs> -```svelte name=src/routes/account/Avatar.svelte - - - -
- {#if avatarUrl} - {avatarUrl - {:else} -
- {/if} - - -
- - -
-
-``` +<$CodeSample +path="/user-management/sveltekit-user-management/src/routes/account/Avatar.svelte" +lines={[[1, -1]]} +meta="name=src/routes/account/Avatar.svelte" +/> ### Add the new widget -And then we can add the widget to the Account page: +Add the widget to the Account page: <$CodeTabs> -```svelte name=src/routes/account/+page.svelte - - - -
-
- - { - profileForm.requestSubmit(); - }} - /> - - - -
-``` +<$CodeSample +path="/user-management/sveltekit-user-management/src/routes/account/+page.svelte" +lines={[[1, 1], [4,4], [31,31],[39,46],[82,82]]} +meta="name=src/routes/account/+page.svelte" +/> diff --git a/apps/docs/content/guides/platform/billing-faq.mdx b/apps/docs/content/guides/platform/billing-faq.mdx index 719cf942f4b..fa0dcde3872 100644 --- a/apps/docs/content/guides/platform/billing-faq.mdx +++ b/apps/docs/content/guides/platform/billing-faq.mdx @@ -64,9 +64,11 @@ If you upgrade your project to a larger instance for 10 hours and then downgrade Read more about [Compute usage](/docs/guides/platform/manage-your-usage/compute). -#### What is unified egress and how is it billed? +#### What is egress and how is it billed? -Unified egress refers to the total egress quota available to each organization. This quota can be utilized for various purposes such as Storage, Realtime, Auth, Functions, Supavisor, Log Drains and Database. Each plan includes a specific egress quota, and any additional usage beyond that quota is billed accordingly. +Egress refers to the total bandwidth (network traffic) quota available to each organization. This quota can be utilized for various purposes such as Storage, Realtime, Auth, Functions, Supavisor, Log Drains and Database. Each plan includes a specific egress quota, and any additional usage beyond that quota is billed accordingly. + +We differentiate between cached (served via our CDN from cache hits) and uncached egress and give quotas for each type and have varying pricing (cached egress is cheaper). Read more about [Egress usage](/docs/guides/platform/manage-your-usage/egress). @@ -128,7 +130,7 @@ The Fair Use Policy is generally applied to all projects of the restricted organ To remove restrictions, you will need to address the issue that caused the restriction. This could be reducing your usage, paying overdue invoices, updating your payment method, or any other issue that caused the restriction. Once the issue is resolved, the restriction will be lifted. -Restrictions due to usage limits are lifted with the next billing cycle as your quota refills at the beginning of each cycle. You can see when your current billing cycle ends on the [billing page](https://supabase.com/dashboard/org/_/billing) under "Upcoming Invoice". If your organization is on the Free Plan, you can also lift restrictions immediately by [upgrading](https://supabase.com/dashboard/org/_/billing?panel=subscriptionPlan) to Pro. +Restrictions due to usage limits are lifted with the next billing cycle as your quota refills at the beginning of each cycle. You can see when your current billing cycle ends on the [billing page](https://supabase.com/dashboard/org/_/billing) under "Upcoming Invoice". You can also lift restrictions immediately by [upgrading](https://supabase.com/dashboard/org/_/billing?panel=subscriptionPlan) to Pro (if on Free Plan) or by [disabling spend cap](https://supabase.com/dashboard/org/_/billing?panel=costControl) (if on Pro Plan with spend cap enabled). ## Reports and invoices diff --git a/apps/docs/content/guides/platform/manage-your-usage/egress.mdx b/apps/docs/content/guides/platform/manage-your-usage/egress.mdx index 71a893036af..b5819a76234 100644 --- a/apps/docs/content/guides/platform/manage-your-usage/egress.mdx +++ b/apps/docs/content/guides/platform/manage-your-usage/egress.mdx @@ -55,25 +55,29 @@ Data pushed to the connected log drain. **Example:** You set up a log drain, each log sent to the log drain is considered egress. You can toggle the GZIP option to reduce egress, in case your provider supports it. +### Cached Egress + +Cached and uncached egress have independent quotas and independent pricing. Cached egress is egress that is served from our CDN via cache hits. Cached egress is typically incurred for storage through our [Smart CDN](/docs/guides/storage/cdn/smart-cdn). + ## How charges are calculated Egress is charged by gigabyte. Charges apply only for usage exceeding your subscription plan's quota. This quota is called the Unified Egress Quota because it can be used across all services (Database, Auth, Storage etc.). ### Usage on your invoice -Usage is shown as "Egress GB" on your invoice. +Usage is shown as "Egress GB" and "Cached Egress GB" on your invoice. ## Pricing - per GB per month. You are only charged for usage exceeding your subscription plan's -quota. + per GB per month for uncached egress, per GB per month +for cached egress. You are only charged for usage exceeding your subscription plan's quota. -| Plan | Unified Egress Quota | Over-Usage per month | -| ---------- | -------------------- | ----------------------------- | -| Free | 5 GB | - | -| Pro | 250 GB | per GB | -| Team | 250 GB | per GB | -| Enterprise | Custom | Custom | +| Plan | Egress Quota (Uncached / Cached) | Over-Usage per month (Uncached / Cached) | +| ---------- | -------------------------------- | ------------------------------------------------------------- | +| Free | 5 GB / 5 GB | - | +| Pro | 250 GB / 250 GB | per GB / per GB | +| Team | 250 GB / 250 GB | per GB / per GB | +| Enterprise | Custom | Custom | ## Billing examples @@ -86,22 +90,24 @@ The organization's Egress usage is within the quota, so no charges for Egress ap | Pro Plan | 1 | | | Compute Hours Micro | 744 hours | | | Egress | 200 GB | | +| Cached Egress | 230 GB | | | **Subtotal** | | **** | | Compute Credits | | - | | **Total** | | **** | ### Exceeding quota -The organization's Egress usage exceeds the quota by 50 GB, incurring charges for this additional usage. +The organization's Egress usage exceeds the uncached egress quota by 50 GB and the cached egress quota by 550 GB, incurring charges for this additional usage. | Line Item | Units | Costs | | ------------------- | --------- | -------------------------- | | Pro Plan | 1 | | | Compute Hours Micro | 744 hours | | | Egress | 300 GB | | -| **Subtotal** | | **** | +| Cached Egress | 800 GB | | +| **Subtotal** | | **** | | Compute Credits | | - | -| **Total** | | **** | +| **Total** | | **** | ## View usage @@ -118,7 +124,7 @@ You can view Egress usage on the [organization's usage page](https://supabase.co zoomable /> -In the Total Egress section, you can see the usage for the selected time period. Hover over a specific date to view a breakdown by service. +In the Total Egress section, you can see the usage for the selected time period. Hover over a specific date to view a breakdown by service. Note that this includes the cached egress. Unified Egress +Separately, you can see the cached egress right below: + +Unified Egress + ### Custom report 1. On the [reports page](https://supabase.com/dashboard/project/_/reports), click **New custom report** in the left navigation menu @@ -144,7 +160,7 @@ In the Total Egress section, you can see the usage for the selected time period. ## Debug usage -To better understand your Egress usage, identify what’s driving the most traffic. Check the most frequent database queries, or analyze the most requested API paths to pinpoint high-bandwidth endpoints. +To better understand your Egress usage, identify what’s driving the most traffic. Check the most frequent database queries, or analyze the most requested API paths to pinpoint high-egress endpoints. ### Frequent database queries diff --git a/apps/docs/content/guides/platform/migrating-to-supabase/firebase-auth.mdx b/apps/docs/content/guides/platform/migrating-to-supabase/firebase-auth.mdx index e33c787357e..bc3dd362dfb 100644 --- a/apps/docs/content/guides/platform/migrating-to-supabase/firebase-auth.mdx +++ b/apps/docs/content/guides/platform/migrating-to-supabase/firebase-auth.mdx @@ -88,4 +88,4 @@ For more advanced migrations, including the use of a middleware server component ## Enterprise -[Contact us](https://forms.supabase.com/enterprise) if you need more help migrating your project. +[Contact us](https://forms.supabase.com/firebase-migration) if you need more help migrating your project. diff --git a/apps/docs/content/guides/platform/migrating-to-supabase/firebase-storage.mdx b/apps/docs/content/guides/platform/migrating-to-supabase/firebase-storage.mdx index 3704d6dd6f7..345e3e6daf9 100644 --- a/apps/docs/content/guides/platform/migrating-to-supabase/firebase-storage.mdx +++ b/apps/docs/content/guides/platform/migrating-to-supabase/firebase-storage.mdx @@ -67,4 +67,4 @@ If the bucket doesn't exist, it's created as a `non-public` bucket. You must set ## Enterprise -[Contact us](https://forms.supabase.com/enterprise) if you need more help migrating your project. +[Contact us](https://forms.supabase.com/firebase-migration) if you need more help migrating your project. diff --git a/apps/docs/content/guides/platform/migrating-to-supabase/firestore-data.mdx b/apps/docs/content/guides/platform/migrating-to-supabase/firestore-data.mdx index 063eecdbad5..3121f96901f 100644 --- a/apps/docs/content/guides/platform/migrating-to-supabase/firestore-data.mdx +++ b/apps/docs/content/guides/platform/migrating-to-supabase/firestore-data.mdx @@ -211,4 +211,4 @@ The result is two separate JSON files: ## Enterprise -[Contact us](https://forms.supabase.com/enterprise) if you need more help migrating your project. +[Contact us](https://forms.supabase.com/firebase-migration) if you need more help migrating your project. diff --git a/apps/docs/content/guides/platform/upgrading.mdx b/apps/docs/content/guides/platform/upgrading.mdx index 0460df6c339..5d001b186ba 100644 --- a/apps/docs/content/guides/platform/upgrading.mdx +++ b/apps/docs/content/guides/platform/upgrading.mdx @@ -164,6 +164,7 @@ In projects using Postgres 17, the following extensions are deprecated: - `plls` - `plv8` - `timescaledb` +- `pgjwt` Existing projects on lower versions of Postgres are not impacted, and the extensions will continue to be supported on projects using Postgres 15, until the end of life of Postgres 15 on the Supabase platform. diff --git a/apps/docs/content/guides/realtime/broadcast.mdx b/apps/docs/content/guides/realtime/broadcast.mdx index 38eb434e251..fb544f58e5f 100644 --- a/apps/docs/content/guides/realtime/broadcast.mdx +++ b/apps/docs/content/guides/realtime/broadcast.mdx @@ -691,3 +691,204 @@ You can pass configuration options while initializing the Supabase Client. Use this to guarantee that the server has received the message before resolving `channelD.send`'s promise. If the `ack` config is not set to `true` when creating the channel, the promise returned by `channelD.send` will resolve immediately. + +### Send messages using REST calls + +You can also send a Broadcast message by making an HTTP request to Realtime servers. This is useful when you want to send messages from your server or client without having to first establish a WebSocket connection. + + + + + + This is currently available only in the Supabase JavaScript client version 2.37.0 and later. + + + + ```js + const channel = supabase.channel('test-channel') + + // No need to subscribe to channel + + channel + .send({ + type: 'broadcast', + event: 'test', + payload: { message: 'Hi' }, + }) + .then((resp) => console.log(resp)) + + // Remember to clean up the channel + + supabase.removeChannel(channel) + + ``` + + + + ```dart + // No need to subscribe to channel + + final channel = supabase.channel('test-channel'); + final res = await channel.sendBroadcastMessage( + event: "test", + payload: { + 'message': 'Hi', + }, + ); + print(res); + ``` + + + + ```swift + let myChannel = await supabase.channel("room-2") { + $0.broadcast.acknowledgeBroadcasts = true + } + + // No need to subscribe to channel + + await myChannel.broadcast(event: "test", message: ["message": "HI"]) + ``` + + + + ```kotlin + val myChannel = supabase.channel("room-2") { + broadcast { + acknowledgeBroadcasts = true + } + } + + // No need to subscribe to channel + + myChannel.broadcast(event = "test", buildJsonObject { + put("message", "Hi") + }) + ``` + + + + Unsupported in Python yet. + + + +## Trigger broadcast messages from your database + + + +This feature is currently in Public Alpha. If you have any issues [submit a support ticket](https://supabase.help). + + + +### How it works + +Broadcast Changes allows you to trigger messages from your database. To achieve it Realtime is directly reading your WAL (Write Append Log) file using a publication against the `realtime.messages` table so whenever a new insert happens a message is sent to connected users. + +It uses partitioned tables per day which allows the deletion your previous messages in a performant way by dropping the physical tables of this partitioned table. Tables older than 3 days old are deleted. + +Broadcasting from the database works like a client-side broadcast, using WebSockets to send JSON packages. [Realtime Authorization](/docs/guides/realtime/authorization) is required and enabled by default to protect your data. + +The database broadcast feature provides two functions to help you send messages: + +- `realtime.send` will insert a message into realtime.messages without a specific format. +- `realtime.broadcast_changes` will insert a message with the required fields to emit database changes to clients. This helps you set up triggers on your tables to emit changes. + +### Broadcasting a message from your database + +The `realtime.send` function provides the most flexibility by allowing you to broadcast messages from your database without a specific format. This allows you to use database broadcast for messages that aren't necessarily tied to the shape of a Postgres row change. + +```sql +SELECT realtime.send ( + to_jsonb ('{}'::text), -- JSONB Payload + 'event', -- Event name + 'topic', -- Topic + FALSE -- Public / Private flag +); +``` + +### Broadcast record changes + +#### Setup realtime authorization + +Realtime Authorization is required and enabled by default. To allow your users to listen to messages from topics, create a RLS (Row Level Security) policy: + +```sql +CREATE POLICY "authenticated can receive broadcasts" +ON "realtime"."messages" +FOR SELECT +TO authenticated +USING ( true ); + +``` + +See the [Realtime Authorization](/docs/guides/realtime/authorization) docs to learn how to set up more specific policies. + +#### Set up trigger function + +First, set up a trigger function that uses `realtime.broadcast_changes` to insert an event whenever it is triggered. The event is set up to include data on the schema, table, operation, and field changes that triggered it. + +For this example use case, we want to have a topic with the name `topic:` to which we're going to broadcast events. + +```sql +CREATE OR REPLACE FUNCTION public.your_table_changes() +RETURNS trigger +SECURITY DEFINER SET search_path = '' +AS $$ +BEGIN + PERFORM realtime.broadcast_changes( + 'topic:' || NEW.id::text, -- topic + TG_OP, -- event + TG_OP, -- operation + TG_TABLE_NAME, -- table + TG_TABLE_SCHEMA, -- schema + NEW, -- new record + OLD -- old record + ); + RETURN NULL; +END; +$$ LANGUAGE plpgsql; +``` + +Of note are the Postgres native trigger special variables used: + +- `TG_OP` - the operation that triggered the function +- `TG_TABLE_NAME` - the table that caused the trigger +- `TG_TABLE_SCHEMA` - the schema of the table that caused the trigger invocation +- `NEW` - the record after the change +- `OLD` - the record before the change + +You can read more about them in this [guide](https://www.postgresql.org/docs/current/plpgsql-trigger.html#PLPGSQL-DML-TRIGGER). + +#### Set up trigger + +Next, set up a trigger so the function runs whenever your target table has a change. + +```sql +CREATE TRIGGER broadcast_changes_for_your_table_trigger +AFTER INSERT OR UPDATE OR DELETE ON public.your_table +FOR EACH ROW +EXECUTE FUNCTION your_table_changes (); +``` + +As you can see, it will be broadcasting all operations so our users will receive events when records are inserted, updated or deleted from `public.your_table` . + +#### Listen on client side + +Finally, client side will requires to be set up to listen to the topic `topic:` to receive the events. + +```jsx +const gameId = 'id' +await supabase.realtime.setAuth() // Needed for Realtime Authorization +const changes = supabase + .channel(`topic:${gameId}`) + .on('broadcast', { event: 'INSERT' }, (payload) => console.log(payload)) + .on('broadcast', { event: 'UPDATE' }, (payload) => console.log(payload)) + .on('broadcast', { event: 'DELETE' }, (payload) => console.log(payload)) + .subscribe() +``` diff --git a/apps/docs/content/guides/realtime/postgres-changes.mdx b/apps/docs/content/guides/realtime/postgres-changes.mdx index 395deda2828..3cdf5b5a1d6 100644 --- a/apps/docs/content/guides/realtime/postgres-changes.mdx +++ b/apps/docs/content/guides/realtime/postgres-changes.mdx @@ -91,8 +91,19 @@ In this example we'll set up a database table, secure it with Row Level Security Go to your project's [Publications settings](https://supabase.com/dashboard/project/_/database/publications), and under `supabase_realtime`, toggle on the tables you want to listen to. + Alternatively, add tables to the `supabase_realtime` publication by running the given SQL: + + + + ```sql + alter publication supabase_realtime + add table your_table_name; + ``` + + + diff --git a/apps/docs/content/guides/storage/production/scaling.mdx b/apps/docs/content/guides/storage/production/scaling.mdx index c5f38e72eae..031ae6a62f9 100644 --- a/apps/docs/content/guides/storage/production/scaling.mdx +++ b/apps/docs/content/guides/storage/production/scaling.mdx @@ -24,6 +24,10 @@ Using the browser cache can effectively lower your egress since the asset remain You have the option to set a maximum upload size for your bucket. Doing this can prevent users from uploading and then downloading excessively large files. You can control the maximum file size by configuring this option at the [bucket level](/docs/guides/storage/buckets/creating-buckets). +#### Smart CDN + +By leveraging our [Smart CDN](/docs/guides/storage/cdn/smart-cdn), you can achieve a higher cache hit rate and therefore lower your egress cached, as we charge less for cached egress (see [egress pricing](/docs/guides/platform/manage-your-usage/egress#pricing)). + ## Optimize listing objects Once you have a substantial number of objects, you might observe that the `supabase.storage.list()` method starts to slow down. This occurs because the endpoint is quite generic and attempts to retrieve both folders and objects in a single query. While this approach is very useful for building features like the Storage viewer on the Supabase dashboard, it can impact performance with a large number of objects. diff --git a/apps/docs/content/guides/storage/serving/bandwidth.mdx b/apps/docs/content/guides/storage/serving/bandwidth.mdx index 740931c242d..021810276c4 100644 --- a/apps/docs/content/guides/storage/serving/bandwidth.mdx +++ b/apps/docs/content/guides/storage/serving/bandwidth.mdx @@ -8,44 +8,52 @@ sidebar_label: 'Bandwidth & Storage Egress' ## Bandwidth & Storage egress -Free Plan Organizations in Supabase have a limit of 5 GB of bandwidth. This limit is calculated by the sum of all the data transferred from the Supabase servers to the client. This includes all the data transferred from the database, storage, and functions. +Free Plan Organizations in Supabase have a limit of 10 GB of bandwidth (5 GB cached + 5 GB uncached). This limit is calculated by the sum of all the data transferred from the Supabase servers to the client. This includes all the data transferred from the database, storage, and functions. -### Checking Storage egress requests in Logs Explorer: +### Checking Storage egress requests in Logs Explorer We have a template query that you can use to get the number of requests for each object in [Logs Explorer](/dashboard/project/_/logs/explorer/templates). ```sql select - r.method as http_verb, - r.path as filepath, + request.method as http_verb, + request.path as filepath, + (responseHeaders.cf_cache_status = 'HIT') as cached, count(*) as num_requests from edge_logs - cross join unnest(metadata) as m - cross join unnest(m.request) as r - cross join unnest(r.headers) as h -where (path like '%storage/v1/object/%' or path like '%storage/v1/render/%') and r.method = 'GET' -group by r.path, r.method + cross join unnest(metadata) as metadata + cross join unnest(metadata.request) as request + cross join unnest(metadata.response) as response + cross join unnest(response.headers) as responseHeaders +where + (path like '%storage/v1/object/%' or path like '%storage/v1/render/%') + and request.method = 'GET' +group by 1, 2, 3 order by num_requests desc limit 100; ``` Example of the output: -``` +```json [ - {"filepath":"/storage/v1/object/sign/large%20bucket/20230902_200037.gif", - "http_verb":"GET", - "num_requests":100 - }, - {"filepath":"/storage/v1/object/public/demob/Sports/volleyball.png", - "http_verb":"GET", - "num_requests":168 - } + { + "filepath": "/storage/v1/object/sign/large%20bucket/20230902_200037.gif", + "http_verb": "GET", + "cached": true, + "num_requests": 100 + }, + { + "filepath": "/storage/v1/object/public/demob/Sports/volleyball.png", + "http_verb": "GET", + "cached": false, + "num_requests": 168 + } ] ``` -### Calculating egress: +### Calculating egress If you already know the size of those files, you can calculate the egress by multiplying the number of requests by the size of the file. You can also get the size of the file with the following cURL: @@ -67,6 +75,6 @@ Total Egress = 395.76MB You can see that these values can get quite large, so it's important to keep track of the egress and optimize the files. -### Optimizing egress: +### Optimizing egress -If you are on the Pro Plan, you can use the [Supabase Image Transformations](/docs/guides/storage/image-transformations) to optimize the images and reduce the egress. +See our [scaling tips for egress](/docs/guides/storage/production/scaling#egress). diff --git a/apps/docs/content/guides/storage/serving/image-transformations.mdx b/apps/docs/content/guides/storage/serving/image-transformations.mdx index 52a2ad73d2c..952d6a79b11 100644 --- a/apps/docs/content/guides/storage/serving/image-transformations.mdx +++ b/apps/docs/content/guides/storage/serving/image-transformations.mdx @@ -259,7 +259,7 @@ response = supabase.storage.from_('bucket').download( When using the image transformation API, Storage will automatically find the best format supported by the client and return that to the client, without any code change. For instance, if you use Chrome when viewing a JPEG image and using transformation options, you'll see that images are automatically optimized as `webp` images. -As a result, this will lower the bandwidth that you send to your users and your application will load much faster. +As a result, this will lower the egress that you send to your users and your application will load much faster. diff --git a/apps/docs/content/guides/telemetry/reports.mdx b/apps/docs/content/guides/telemetry/reports.mdx index 34fbd117671..49ab17a59aa 100644 --- a/apps/docs/content/guides/telemetry/reports.mdx +++ b/apps/docs/content/guides/telemetry/reports.mdx @@ -270,7 +270,7 @@ The Storage report provides visibility into how your Supabase Storage is being u | --------------- | ------------------------------------------ | ------------------------------------------------------ | | Total Requests | Overall request volume to Storage | Traffic patterns and usage trends | | Response Speed | Average response time for storage requests | Performance bottlenecks and optimization opportunities | -| Network Traffic | Ingress and egress bandwidth usage | Data transfer costs and CDN effectiveness | +| Network Traffic | Ingress and egress usage | Data transfer costs and CDN effectiveness | | Request Caching | Cache hit rates and miss patterns | CDN performance and cost optimization | | Top Routes | Most frequently accessed storage paths | Popular content and usage patterns | @@ -305,5 +305,5 @@ The API Gateway report analyzes traffic patterns and performance characteristics | Total Requests | Overall API request volume | Traffic patterns and growth trends | | Response Errors | Error rates with 4XX and 5XX status codes | API reliability and user experience issues | | Response Speed | Average API response times | Performance bottlenecks and optimization targets | -| Network Traffic | Request and response bandwidth usage | Data transfer patterns and cost implications | +| Network Traffic | Request and response egress usage | Data transfer patterns and cost implications | | Top Routes | Most frequently accessed API endpoints | Usage patterns and optimization priorities | diff --git a/apps/docs/content/troubleshooting/all-about-supabase-egress-a_Sg_e.mdx b/apps/docs/content/troubleshooting/all-about-supabase-egress-a_Sg_e.mdx index 78cd4f9dde7..974ce7498ac 100644 --- a/apps/docs/content/troubleshooting/all-about-supabase-egress-a_Sg_e.mdx +++ b/apps/docs/content/troubleshooting/all-about-supabase-egress-a_Sg_e.mdx @@ -31,4 +31,10 @@ While pointing out the exact cause for egress may not be straightforward, there - Reduce the number of queries/calls by optimising client code or use caches to reduce the number of requests/queries being done: https://github.com/psteinroe/supabase-cache-helpers/ - In case of update/insert queries, if you don’t need the entire row to be returned, configure your ORM/queries to not return the entire row - In case of running manual backups through Supavisor, remove unneeded tables and/or reduce the frequency -- For Storage, if you start using the [Smart CDN](https://supabase.com/docs/guides/storage/cdn/smart-cdn) Storage Egress usage can be managed. You can also use the [Supabase Image Transformations](https://supabase.com/docs/guides/storage/image-transformations) to optimize the images and reduce the egress. +- For Storage, if you start using the [Smart CDN](https://supabase.com/docs/guides/storage/cdn/smart-cdn) Storage Egress usage can be reduced. You can also use the [Supabase Image Transformations](https://supabase.com/docs/guides/storage/image-transformations) to optimize the images and reduce the egress. + +**Cached vs uncached egress** + +We differentiate between cached and uncached egress. Cached egress refers to egress that is served via our CDN and hits the cache. Uncached egress, on the other hand, refers to egress that is not served from the cache and requires a fresh request to the origin server. + +Your plan includes a quota for both cached and uncached egress and these are independent. Cached egress is also cheaper in case you exceed your quota. diff --git a/apps/docs/docs/ref/swift/installing.mdx b/apps/docs/docs/ref/swift/installing.mdx index cbc57f06a7b..2ba0b701403 100644 --- a/apps/docs/docs/ref/swift/installing.mdx +++ b/apps/docs/docs/ref/swift/installing.mdx @@ -20,6 +20,10 @@ custom_edit_url: https://github.com/supabase/supabase/edit/master/web/spec/supab - `Functions` - `Storage` + If you use Xcode, follow [Apple's dependencies guide](https://developer.apple.com/documentation/swift_packages/adding_package_dependencies_to_your_app) to add supabase-swift to your project. Use https://github.com/supabase-community/supabase-swift.git for the url when Xcode asks. + + If you don't want the full Supabase environment, you can add individual packages, such as Functions, `Auth`, `Realtime`, `Storage`, or `PostgREST`. + diff --git a/apps/docs/features/docs/GuidesMdx.template.tsx b/apps/docs/features/docs/GuidesMdx.template.tsx index e7067e6815f..76fbe6d397c 100644 --- a/apps/docs/features/docs/GuidesMdx.template.tsx +++ b/apps/docs/features/docs/GuidesMdx.template.tsx @@ -90,7 +90,9 @@ const GuideTemplate = ({ meta, content, children, editLink, mdxOptions }: GuideT )}
- {content && } + {content && ( + x} /> + )} {children}
diff --git a/apps/docs/features/docs/GuidesMdx.utils.tsx b/apps/docs/features/docs/GuidesMdx.utils.tsx index 6dfce853884..50b5dede231 100644 --- a/apps/docs/features/docs/GuidesMdx.utils.tsx +++ b/apps/docs/features/docs/GuidesMdx.utils.tsx @@ -1,18 +1,20 @@ -import matter from 'gray-matter' +import * as Sentry from '@sentry/nextjs' import { fromMarkdown } from 'mdast-util-from-markdown' import { gfmFromMarkdown } from 'mdast-util-gfm' import { gfm } from 'micromark-extension-gfm' import { type Metadata, type ResolvingMetadata } from 'next' import { notFound } from 'next/navigation' -import { readFile, readdir } from 'node:fs/promises' -import { extname, join, sep } from 'node:path' +import { readdir } from 'node:fs/promises' +import { extname, join, relative, sep } from 'node:path' +import { extractMessageFromAnyError, FileNotFoundError } from '~/app/api/utils' import { pluckPromise } from '~/features/helpers.fn' import { cache_fullProcess_withDevCacheBust, existsFile } from '~/features/helpers.fs' import type { OrPromise } from '~/features/helpers.types' import { generateOpenGraphImageMeta } from '~/features/seo/openGraph' import { BASE_PATH } from '~/lib/constants' import { GUIDES_DIRECTORY, isValidGuideFrontmatter, type GuideFrontmatter } from '~/lib/docs' +import { GuideModelLoader } from '~/resources/guide/guideModelLoader' import { newEditLink } from './GuidesMdx.template' const PUBLISHED_SECTIONS = [ @@ -51,31 +53,40 @@ const getGuidesMarkdownInternal = async (slug: string[]) => { notFound() } - let mdx: string try { - mdx = await readFile(fullPath, 'utf-8') - } catch { - // Not using console.error because this includes pages that are genuine - // 404s and clutters up the logs - console.log('Error reading Markdown at path: %s', fullPath) + const guide = (await GuideModelLoader.fromFs(relative(GUIDES_DIRECTORY, fullPath))).unwrap() + const content = guide.content ?? '' + const meta = guide.metadata ?? {} + + if (!isValidGuideFrontmatter(meta)) { + throw Error(`Type of frontmatter is not valid for path: ${fullPath}`) + } + + const editLink = newEditLink( + `supabase/supabase/blob/master/apps/docs/content/guides/${relPath}.mdx` + ) + + return { + pathname: `/guides/${slug.join('/')}` satisfies `/${string}`, + meta, + content, + editLink, + } + } catch (error: unknown) { + if (error instanceof Error && error.cause instanceof FileNotFoundError) { + // Not using console.error because this includes pages that are genuine + // 404s and clutters up the logs + console.log('Could not read Markdown at path: %s', fullPath) + } else { + console.error( + 'Error processing Markdown file at path: %s:\n\t%s', + fullPath, + extractMessageFromAnyError(error) + ) + Sentry.captureException(error) + } notFound() } - - const editLink = newEditLink( - `supabase/supabase/blob/master/apps/docs/content/guides/${relPath}.mdx` - ) - - const { data: meta, content } = matter(mdx) - if (!isValidGuideFrontmatter(meta)) { - throw Error('Type of frontmatter is not valid') - } - - return { - pathname: `/guides/${slug.join('/')}` satisfies `/${string}`, - meta, - content, - editLink, - } } /** diff --git a/apps/docs/features/docs/Reference.navigation.client.tsx b/apps/docs/features/docs/Reference.navigation.client.tsx index b714555561a..485f6542c78 100644 --- a/apps/docs/features/docs/Reference.navigation.client.tsx +++ b/apps/docs/features/docs/Reference.navigation.client.tsx @@ -214,7 +214,7 @@ export function RefLink({ { return this as unknown as Result } + async mapAsync(fn: (data: Ok) => Promise): Promise> { + if (this.isOk()) return Result.ok(await fn(this.internal.data!)) + return this as unknown as Result + } + mapError(fn: (error: Error) => MappedError): Result { if (this.isOk()) return this as unknown as Result return Result.error(fn(this.internal.error!)) @@ -147,6 +152,27 @@ export class Result { return this.internal.data! } + unwrapOr(deflt: () => Ok): Ok { + if (this.isOk()) return this.internal.data! + return deflt() + } + + unwrapError(): Error { + if (this.isOk()) { + throw new Error(`UnwrapError called on Ok`) + } + return this.internal.error! + } + + unwrapErrorSafe(): Error | null { + return this.internal.error + } + + unwrapEither(): Ok | Error { + if (this.isOk()) return this.unwrap() + return this.unwrapError() + } + join( other: Result ): Result<[Ok, OtherOk], [Error, OtherError]> { @@ -158,3 +184,47 @@ export class Result { return Result.ok([this.internal.data!, other.internal.data!]) } } + +export class Both { + private internal: { + left: Left + right: Right + } + + constructor(left: Left, right: Right) { + this.internal = { + left, + right, + } + } + + mapLeft(fn: (left: Left) => NewLeft): Both { + return new Both(fn(this.internal.left), this.internal.right) + } + + mapRight(fn: (right: Right) => NewRight): Both { + return new Both(this.internal.left, fn(this.internal.right)) + } + + async mapLeftAsync(fn: (left: Left) => Promise): Promise> { + const res = await fn(this.internal.left) + return new Both(res, this.internal.right) + } + + unwrapLeft(): Left { + return this.internal.left + } + + unwrapRight(): Right { + return this.internal.right + } + + combine(fn: (left: Left, right: Right) => Output): Output { + return fn(this.internal.left, this.internal.right) + } + + intoResult(): Result { + if (this.internal.right) return Result.error(this.internal.right) + return Result.ok(this.internal.left) + } +} diff --git a/apps/docs/features/helpers.types.ts b/apps/docs/features/helpers.types.ts index 252e21955bb..45bc7c18ce0 100644 --- a/apps/docs/features/helpers.types.ts +++ b/apps/docs/features/helpers.types.ts @@ -1,7 +1,5 @@ -type OrPromise = T | Promise +export type Json = string | number | boolean | { [key: string]: Json } | Json[] -type Json = string | number | boolean | { [key: string]: Json } | Json[] +export type OrPromise = T | Promise -type WithRequired = T & { [P in K]-?: T[P] } - -export type { Json, OrPromise, WithRequired } +export type WithRequired = T & { [P in K]-?: T[P] } diff --git a/apps/docs/lib/openAi.ts b/apps/docs/lib/openAi.ts index 61b51f11dd2..45e361257c2 100644 --- a/apps/docs/lib/openAi.ts +++ b/apps/docs/lib/openAi.ts @@ -10,13 +10,18 @@ import { Result } from '~/features/helpers.fn' type Embedding = Array +export interface EmbeddingWithTokens { + embedding: Embedding + token_count: number +} + interface ModerationFlaggedDetails { flagged: boolean categories: OpenAI.Moderations.Moderation.Categories } export interface OpenAIClientInterface { - createContentEmbedding(text: string): Promise> + createContentEmbedding(text: string): Promise> } let openAIClient: OpenAIClientInterface | null @@ -26,7 +31,9 @@ class OpenAIClient implements OpenAIClientInterface { constructor(private client: OpenAI) {} - async createContentEmbedding(text: string): Promise> { + async createContentEmbedding( + text: string + ): Promise> { return await Result.tryCatchFlat( this.createContentEmbeddingImpl.bind(this), convertUnknownToApiError, @@ -36,7 +43,7 @@ class OpenAIClient implements OpenAIClientInterface { private async createContentEmbeddingImpl( text: string - ): Promise>> { + ): Promise>> { const query = text.trim() const moderationResponse = await this.client.moderations.create({ input: query }) @@ -55,7 +62,12 @@ class OpenAIClient implements OpenAIClientInterface { input: query, }) const [{ embedding: queryEmbedding }] = embeddingsResponse.data - return Result.ok(queryEmbedding) + const tokenCount = embeddingsResponse.usage.total_tokens + + return Result.ok({ + embedding: queryEmbedding, + token_count: tokenCount, + }) } } diff --git a/apps/docs/lib/supabase.ts b/apps/docs/lib/supabase.ts index e7c1768a374..78f371e4b85 100644 --- a/apps/docs/lib/supabase.ts +++ b/apps/docs/lib/supabase.ts @@ -1,11 +1,32 @@ import { createClient, type SupabaseClient } from '@supabase/supabase-js' import { type Database as DatabaseGenerated } from 'common' -type Database = { +export type Database = { content: DatabaseGenerated['content'] graphql_public: DatabaseGenerated['graphql_public'] public: { - Tables: DatabaseGenerated['public']['Tables'] + Tables: Omit & { + page_section: Omit< + DatabaseGenerated['public']['Tables']['page_section'], + 'Row' | 'Insert' | 'Update' + > & { + Row: Omit & { + embedding: Array | null + } + Insert: Omit< + DatabaseGenerated['public']['Tables']['page_section']['Insert'], + 'embedding' + > & { + embedding?: Array | null + } + Update: Omit< + DatabaseGenerated['public']['Tables']['page_section']['Update'], + 'embedding' + > & { + embedding?: Array | null + } + } + } Views: DatabaseGenerated['public']['Views'] Functions: Omit< DatabaseGenerated['public']['Functions'], diff --git a/apps/docs/lib/supabaseAdmin.ts b/apps/docs/lib/supabaseAdmin.ts index 334fe3035a5..3df3802759e 100644 --- a/apps/docs/lib/supabaseAdmin.ts +++ b/apps/docs/lib/supabaseAdmin.ts @@ -2,7 +2,7 @@ import 'server-only' import { createClient, type SupabaseClient } from '@supabase/supabase-js' -import { type Database } from 'common' +import { type Database } from '~/lib/supabase' let supabaseAdminClient: SupabaseClient | null = null diff --git a/apps/docs/package.json b/apps/docs/package.json index 0d560b06ff9..82594065ea1 100644 --- a/apps/docs/package.json +++ b/apps/docs/package.json @@ -6,7 +6,7 @@ "scripts": { "build": "next build", "build:analyze": "ANALYZE=true next build", - "build:llms": "tsx ./scripts/llms.ts", + "build:llms": "tsx --conditions=react-server ./scripts/llms.ts", "build:sitemap": "tsx ./internals/generate-sitemap.ts", "clean": "rimraf .next .turbo node_modules features/docs/generated examples __generated__", "codegen:examples": "shx cp -r ../../examples ./examples", @@ -16,7 +16,7 @@ "dev": "concurrently --kill-others \"next dev --port 3001\" \"pnpm run dev:watch:troubleshooting\"", "dev:secrets:pull": "AWS_PROFILE=supabase-dev node ../../scripts/getSecrets.js -n local/docs", "dev:watch:troubleshooting": "node ./scripts/troubleshooting/watch.mjs", - "embeddings": "tsx scripts/search/generate-embeddings.ts", + "embeddings": "tsx --conditions=react-server scripts/search/generate-embeddings.ts", "embeddings:refresh": "pnpm run embeddings --refresh", "last-changed": "tsx scripts/last-changed.ts", "last-changed:reset": "pnpm run last-changed -- --reset", @@ -51,7 +51,7 @@ "@octokit/plugin-paginate-graphql": "^4.0.0", "@radix-ui/react-accordion": "^1.1.2", "@radix-ui/react-collapsible": "^1.0.3", - "@sentry/nextjs": "^9.15.0", + "@sentry/nextjs": "^10.3.0", "@supabase/supabase-js": "catalog:", "@tailwindcss/container-queries": "^0.1.1", "@tanstack/react-query": "^5.13.4", @@ -138,6 +138,7 @@ "@types/react-copy-to-clipboard": "^5.0.4", "@types/react-dom": "catalog:", "@types/unist": "^2.0.6", + "@types/uuid": "^10.0.0", "api-types": "workspace:*", "cheerio": "^1.0.0-rc.12", "chokidar": "^4.0.3", diff --git a/apps/docs/public/humans.txt b/apps/docs/public/humans.txt index 1d2fe38b68b..bcfefdf7533 100644 --- a/apps/docs/public/humans.txt +++ b/apps/docs/public/humans.txt @@ -38,6 +38,7 @@ Deepthi Sigireddi Deji I Div Arora Divit D +Douglas Hunley Eduardo Gurgel Egor Romanov Eleftheria Trivyzaki @@ -85,6 +86,7 @@ Luca Forstner Manan Gupta Margarita Sandomirskaia Mark Burggraf +Matt Rossman Monica Khoury Mykhailo Mischa Lieibenson Nyannyacha diff --git a/apps/docs/public/img/guides/platform/cached-egress--light.png b/apps/docs/public/img/guides/platform/cached-egress--light.png new file mode 100644 index 00000000000..5da6064b7a9 Binary files /dev/null and b/apps/docs/public/img/guides/platform/cached-egress--light.png differ diff --git a/apps/docs/public/img/guides/platform/cached-egress.png b/apps/docs/public/img/guides/platform/cached-egress.png new file mode 100644 index 00000000000..60ed3968ec4 Binary files /dev/null and b/apps/docs/public/img/guides/platform/cached-egress.png differ diff --git a/apps/docs/resources/globalSearch/globalSearchModel.ts b/apps/docs/resources/globalSearch/globalSearchModel.ts index 8c63f28f493..c6243d530c9 100644 --- a/apps/docs/resources/globalSearch/globalSearchModel.ts +++ b/apps/docs/resources/globalSearch/globalSearchModel.ts @@ -22,7 +22,7 @@ export abstract class SearchResultModel { const includeFullContent = requestedFields.includes('content') const embeddingResult = await openAI().createContentEmbedding(query) - return embeddingResult.flatMapAsync(async (embedding) => { + return embeddingResult.flatMapAsync(async ({ embedding }) => { const matchResult = new Result( await supabase().rpc('search_content', { embedding, @@ -49,7 +49,7 @@ export abstract class SearchResultModel { const includeFullContent = requestedFields.includes('content') const embeddingResult = await openAI().createContentEmbedding(query) - return embeddingResult.flatMapAsync(async (embedding) => { + return embeddingResult.flatMapAsync(async ({ embedding }) => { const matchResult = new Result( await supabase().rpc('search_content_hybrid', { query_text: query, diff --git a/apps/docs/resources/guide/guideModel.ts b/apps/docs/resources/guide/guideModel.ts index 5201d7a9cc0..dbdfff02aea 100644 --- a/apps/docs/resources/guide/guideModel.ts +++ b/apps/docs/resources/guide/guideModel.ts @@ -3,23 +3,31 @@ import { type SearchResultInterface } from '../globalSearch/globalSearchInterfac export class GuideModel implements SearchResultInterface { public title?: string public href?: string + public checksum?: string public content?: string + public metadata?: Record public subsections: Array constructor({ title, href, + checksum, content, + metadata, subsections, }: { title?: string href?: string + checksum?: string content?: string + metadata?: Record subsections?: Array<{ title?: string; href?: string; content?: string }> }) { this.title = title this.href = href + this.checksum = checksum this.content = content + this.metadata = metadata this.subsections = subsections?.map((subsection) => new SubsectionModel(subsection)) ?? [] } } diff --git a/apps/docs/resources/guide/guideModelLoader.ts b/apps/docs/resources/guide/guideModelLoader.ts new file mode 100644 index 00000000000..4ba9674de1e --- /dev/null +++ b/apps/docs/resources/guide/guideModelLoader.ts @@ -0,0 +1,201 @@ +import matter from 'gray-matter' +import { promises as fs } from 'node:fs' +import { join, relative } from 'node:path' + +import { extractMessageFromAnyError, FileNotFoundError, MultiError } from '~/app/api/utils' +import { preprocessMdxWithDefaults } from '~/features/directives/utils' +import { Both, Result } from '~/features/helpers.fn' +import { GUIDES_DIRECTORY } from '~/lib/docs' +import { processMdx } from '~/scripts/helpers.mdx' +import { GuideModel } from './guideModel' + +/** + * Determines if a file is hidden. + * + * A file is hidden if its name, or the name of any of its parent directories, + * starts with an underscore. + */ +function isHiddenFile(path: string): boolean { + return path.split('/').some((part) => part.startsWith('_')) +} + +/** + * Recursively walks a directory and collects all .mdx files that are not hidden. + */ +async function walkMdxFiles( + dir: string, + multiError: { current: MultiError | null } +): Promise> { + const readDirResult = await Result.tryCatch( + () => fs.readdir(dir, { recursive: true }), + (error) => error + ) + + return readDirResult.match( + (allPaths) => { + const mdxFiles: string[] = [] + + for (const relativePath of allPaths) { + if (isHiddenFile(relativePath)) { + continue + } + + // Only include .mdx files + if (relativePath.endsWith('.mdx')) { + mdxFiles.push(join(dir, relativePath)) + } + } + + return mdxFiles + }, + (error) => { + // If we can't read the directory, add it to the error collection + ;(multiError.current ??= new MultiError('Failed to load some guides:')).appendError( + `Failed to read directory ${dir}: ${extractMessageFromAnyError(error)}`, + error + ) + return [] + } + ) +} + +/** + * Node.js-specific loader for GuideModel instances from the filesystem. + * This class contains all the filesystem operations that require Node.js capabilities. + */ +export class GuideModelLoader { + /** + * Creates a GuideModel instance by loading and processing a markdown file from the filesystem. + * + * @param relPath - Relative path to the markdown file within the guides directory (e.g., "auth/users.mdx") + * @returns A Result containing either the processed GuideModel or an error message + * + * @example + * ```typescript + * const result = await GuideModelLoader.fromFs('auth/users.mdx') + * result.match( + * (guide) => console.log(guide.title, guide.subsections.length), + * (error) => console.error(error) + * ) + * ``` + */ + static async fromFs(relPath: string): Promise> { + return Result.tryCatch( + async () => { + // Read the markdown file from the guides directory + const filePath = join(GUIDES_DIRECTORY, relPath) + const fileContent = await fs.readFile(filePath, 'utf-8') + + // Parse frontmatter using gray-matter + const { data: metadata, content: rawContent } = matter(fileContent) + + // Replace partials and code samples using directives + const processedContent = await preprocessMdxWithDefaults(rawContent) + + // Process MDX to get chunked sections for embedding + const { sections } = await processMdx(processedContent) + + // Create subsections from the chunked sections + const subsections = sections.map((section) => ({ + title: section.heading, + href: section.slug, + content: section.content, + })) + + // Extract title from metadata or first heading + const title = metadata.title || sections.find((s) => s.heading)?.heading + + // Create href from relative path (remove .mdx extension) + const href = `https://supabase.com/docs/guides/${relPath.replace(/\.mdx?$/, '')}` + + return new GuideModel({ + title, + href, + content: processedContent, + metadata, + subsections, + }) + }, + (error) => { + if (error instanceof Error && 'code' in error && error.code === 'ENOENT') { + return new FileNotFoundError('', error) + } + return new Error( + `Failed to load guide from ${relPath}: ${extractMessageFromAnyError(error)}`, + { + cause: error, + } + ) + } + ) + } + + /** + * Loads GuideModels from a list of file paths in parallel, collecting any + * errors without stopping. + */ + private static async loadGuides( + filePaths: Array, + multiError: { current: MultiError | null } + ): Promise> { + const loadPromises = filePaths.map(async (filePath) => { + const relPath = relative(GUIDES_DIRECTORY, filePath) + return this.fromFs(relPath) + }) + + const results = await Promise.all(loadPromises) + const guides: Array = [] + + results.forEach((result, index) => { + const relPath = relative(GUIDES_DIRECTORY, filePaths[index]) + + result.match( + (guide) => guides.push(guide), + (error) => { + ;(multiError.current ??= new MultiError('Failed to load some guides:')).appendError( + `Failed to load ${relPath}: ${extractMessageFromAnyError(error)}`, + error + ) + } + ) + }) + + return guides + } + + /** + * Loads all guide models from the filesystem by walking the content directory. + * + * This method recursively walks the guides directory (or a specific section + * subdirectory) and loads all non-hidden .mdx files. + * + * If errors occur while loading individual files, they are collected but + * don't prevent other files from loading. + * + * @param section - Optional section name to limit walking to a specific + * subdirectory (e.g., "database", "auth") + * @returns A Both containing [successful GuideModels, MultiError with all + * failures or null if no errors] + * + * @example + * ```typescript + * // Load all guides + * const guides = (await GuideModelLoader.allFromFs()).unwrapLeft() + * + * // Load only database guides + * const dbGuides = (await GuideModelLoader.allFromFs('database')).unwrapLeft() + * ``` + */ + static async allFromFs(section?: string): Promise, MultiError | null>> { + const searchDir = section ? join(GUIDES_DIRECTORY, section) : GUIDES_DIRECTORY + const multiError = { current: null as MultiError | null } + + // Get all .mdx files in the search directory + const mdxFiles = await walkMdxFiles(searchDir, multiError) + + // Load each file and collect results + const guides = await this.loadGuides(mdxFiles, multiError) + + return new Both(guides, multiError.current) + } +} diff --git a/apps/docs/scripts/helpers.mdx.ts b/apps/docs/scripts/helpers.mdx.ts index c6d9ce672ef..b71d01d8a09 100644 --- a/apps/docs/scripts/helpers.mdx.ts +++ b/apps/docs/scripts/helpers.mdx.ts @@ -1,17 +1,12 @@ -import { createHash } from 'node:crypto' -import { ObjectExpression } from 'estree' import GithubSlugger from 'github-slugger' import matter from 'gray-matter' import { type Content, type Root } from 'mdast' import { fromMarkdown } from 'mdast-util-from-markdown' import { toMarkdown } from 'mdast-util-to-markdown' -import { mdxFromMarkdown, type MdxjsEsm } from 'mdast-util-mdx' +import { mdxFromMarkdown, mdxToMarkdown } from 'mdast-util-mdx' import { toString } from 'mdast-util-to-string' import { mdxjs } from 'micromark-extension-mdxjs' import { u } from 'unist-builder' -import { filter } from 'unist-util-filter' - -type Json = Record type Section = { content: string @@ -21,17 +16,25 @@ type Section = { export type ProcessedMdx = { checksum: string - meta: Json + meta: Record sections: Section[] } +async function createHash(content: string): Promise { + const encoder = new TextEncoder() + const data = encoder.encode(content) + const hashBuffer = await crypto.subtle.digest('SHA-256', data) + const hashArray = Array.from(new Uint8Array(hashBuffer)) + return hashArray.map((byte) => byte.toString(16).padStart(2, '0')).join('') +} + /** * Process MDX content. * * Splits MDX content into sections based on headings, and calculates checksum. */ -function processMdx(content: string, options?: { yaml?: boolean }): ProcessedMdx { - const checksum = createHash('sha256').update(content).digest('base64') +async function processMdx(content: string, options?: { yaml?: boolean }): Promise { + const checksum = await createHash(content) let frontmatter: Record = {} if (options?.yaml) { @@ -45,43 +48,15 @@ function processMdx(content: string, options?: { yaml?: boolean }): ProcessedMdx mdastExtensions: [mdxFromMarkdown()], }) - let meta: Record | undefined - if (options?.yaml) { - meta = frontmatter - } else { - meta = extractMetaExport(mdxTree) - } - - const serializableMeta: Json = meta && JSON.parse(JSON.stringify(meta)) - - // Remove all MDX elements from markdown - const mdTree = filter( - mdxTree, - (node) => - ![ - 'mdxjsEsm', - 'mdxJsxFlowElement', - 'mdxJsxTextElement', - 'mdxFlowExpression', - 'mdxTextExpression', - ].includes(node.type) - ) - - if (!mdTree) { - return { - checksum, - meta: serializableMeta, - sections: [], - } - } - - const sectionTrees = splitTreeBy(mdTree, (node) => node.type === 'heading') + const sectionTrees = splitTreeBy(mdxTree, (node) => node.type === 'heading') const slugger = new GithubSlugger() const sections = sectionTrees.map((tree) => { const [firstNode] = tree.children - const content = toMarkdown(tree) + const content = toMarkdown(tree, { + extensions: [mdxToMarkdown()], + }) const rawHeading: string | undefined = firstNode.type === 'heading' ? toString(firstNode) : undefined @@ -103,74 +78,11 @@ function processMdx(content: string, options?: { yaml?: boolean }): ProcessedMdx return { checksum, - meta: serializableMeta, sections, + meta: frontmatter, } } -/** - * Extracts the `meta` ESM export from the MDX file. - * - * This info is akin to frontmatter. - */ -function extractMetaExport(mdxTree: Root) { - const metaExportNode = mdxTree.children.find((node): node is MdxjsEsm => { - return ( - node.type === 'mdxjsEsm' && - node.data?.estree?.body[0]?.type === 'ExportNamedDeclaration' && - node.data.estree.body[0].declaration?.type === 'VariableDeclaration' && - node.data.estree.body[0].declaration.declarations[0]?.id.type === 'Identifier' && - node.data.estree.body[0].declaration.declarations[0].id.name === 'meta' - ) - }) - - if (!metaExportNode) { - return undefined - } - - const objectExpression = - (metaExportNode.data?.estree?.body[0]?.type === 'ExportNamedDeclaration' && - metaExportNode.data.estree.body[0].declaration?.type === 'VariableDeclaration' && - metaExportNode.data.estree.body[0].declaration.declarations[0]?.id.type === 'Identifier' && - metaExportNode.data.estree.body[0].declaration.declarations[0].id.name === 'meta' && - metaExportNode.data.estree.body[0].declaration.declarations[0].init?.type === - 'ObjectExpression' && - metaExportNode.data.estree.body[0].declaration.declarations[0].init) || - undefined - - if (!objectExpression) { - return undefined - } - - return getObjectFromExpression(objectExpression) -} - -/** - * Extracts ES literals from an `estree` `ObjectExpression` - * into a plain JavaScript object. - */ -function getObjectFromExpression(node: ObjectExpression) { - return node.properties.reduce< - Record - >((object, property) => { - if (property.type !== 'Property') { - return object - } - - const key = (property.key.type === 'Identifier' && property.key.name) || undefined - const value = (property.value.type === 'Literal' && property.value.value) || undefined - - if (!key) { - return object - } - - return { - ...object, - [key]: value, - } - }, {}) -} - /** * Splits a `mdast` tree into multiple trees based on * a predicate function. Will include the splitting node @@ -210,4 +122,4 @@ function parseHeading(heading: string): { heading: string; customAnchor?: string } export { processMdx } -export type { Json, Section } +export type { Section } diff --git a/apps/docs/scripts/search/generate-embeddings.ts b/apps/docs/scripts/search/generate-embeddings.ts index 288c86b35e8..d8a7bfdf909 100644 --- a/apps/docs/scripts/search/generate-embeddings.ts +++ b/apps/docs/scripts/search/generate-embeddings.ts @@ -4,7 +4,7 @@ import { createClient } from '@supabase/supabase-js' import { parseArgs } from 'node:util' import { OpenAI } from 'openai' import { v4 as uuidv4 } from 'uuid' -import type { Json, Section } from '../helpers.mdx.js' +import type { Section } from '../helpers.mdx.js' import { fetchAllSources } from './sources/index.js' const args = parseArgs({ @@ -78,8 +78,8 @@ async function generateEmbeddings() { checksum: string sections: Section[] ragIgnore?: boolean - meta?: Json - } = embeddingSource.process() + meta?: Record + } = await embeddingSource.process() // Check for existing page in DB and compare checksums const { error: fetchPageError, data: existingPage } = await supabaseClient diff --git a/apps/docs/scripts/search/sources/base.ts b/apps/docs/scripts/search/sources/base.ts index 32b5aad9e9c..a7835ccd924 100644 --- a/apps/docs/scripts/search/sources/base.ts +++ b/apps/docs/scripts/search/sources/base.ts @@ -1,4 +1,4 @@ -import type { Json, Section } from '../../helpers.mdx.js' +import type { Section } from '../../helpers.mdx.js' export abstract class BaseLoader { type: string @@ -14,7 +14,7 @@ export abstract class BaseLoader { export abstract class BaseSource { type: string checksum?: string - meta?: Json + meta?: Record sections?: Section[] constructor( @@ -22,7 +22,12 @@ export abstract class BaseSource { public path: string ) {} - abstract process(): { checksum: string; meta?: Json; ragIgnore?: boolean; sections: Section[] } + abstract process(): Promise<{ + checksum: string + meta?: Record + ragIgnore?: boolean + sections: Section[] + }> abstract extractIndexedContent(): string } diff --git a/apps/docs/scripts/search/sources/github-discussion.ts b/apps/docs/scripts/search/sources/github-discussion.ts index fe3db305ef2..fc29ce7e7c8 100644 --- a/apps/docs/scripts/search/sources/github-discussion.ts +++ b/apps/docs/scripts/search/sources/github-discussion.ts @@ -104,7 +104,7 @@ export class GitHubDiscussionSource extends BaseSource { super(source, path) } - process() { + async process() { const { id, title, updatedAt, body, databaseId } = this.discussion const checksum = createHash('sha256').update(updatedAt).digest('base64') diff --git a/apps/docs/scripts/search/sources/lint-warnings-guide.ts b/apps/docs/scripts/search/sources/lint-warnings-guide.ts index 77713504e01..b329f0be819 100644 --- a/apps/docs/scripts/search/sources/lint-warnings-guide.ts +++ b/apps/docs/scripts/search/sources/lint-warnings-guide.ts @@ -92,7 +92,7 @@ export class LintWarningsGuideSource extends BaseSource { super(source, path) } - process() { + async process() { this.checksum = createHash('sha256').update(this.lint.content).digest('base64') this.meta = { diff --git a/apps/docs/scripts/search/sources/markdown.ts b/apps/docs/scripts/search/sources/markdown.ts index 74b5b8fe255..0c6f759d6e3 100644 --- a/apps/docs/scripts/search/sources/markdown.ts +++ b/apps/docs/scripts/search/sources/markdown.ts @@ -1,5 +1,5 @@ -import { readFile } from 'fs/promises' -import { processMdx } from '../../helpers.mdx.js' +import { SubsectionModel } from '../../../resources/guide/guideModel.js' +import { GuideModelLoader } from '../../../resources/guide/guideModelLoader.js' import { BaseLoader, BaseSource } from './base.js' export class MarkdownLoader extends BaseLoader { @@ -15,8 +15,22 @@ export class MarkdownLoader extends BaseLoader { } async load() { - const contents = await readFile(this.filePath, 'utf8') - return [new MarkdownSource(this.source, this.path, contents, this.options)] + const guide = ( + await GuideModelLoader.fromFs(this.filePath.replace(/^content\/guides/, '')) + ).unwrap() + return [ + new MarkdownSource( + this.source, + this.path, + guide.content ?? '', + { + checksum: guide.checksum, + meta: guide.metadata, + sections: guide.subsections, + }, + this.options + ), + ] } } @@ -27,19 +41,29 @@ export class MarkdownSource extends BaseSource { source: string, path: string, public contents: string, + { + checksum, + meta, + sections, + }: { checksum?: string; meta?: Record; sections: Array }, public options?: { yaml?: boolean } ) { super(source, path) + this.checksum = checksum + this.meta = meta ?? {} + this.sections = sections.map((section) => ({ + content: section.content ?? '', + heading: section.title, + slug: section.href, + })) } - process() { - const { checksum, meta, sections } = processMdx(this.contents, this.options) - - this.checksum = checksum - this.meta = meta - this.sections = sections - - return { checksum, meta, sections } + async process() { + return { + checksum: this.checksum ?? '', + meta: this.meta, + sections: this.sections ?? [], + } } extractIndexedContent(): string { diff --git a/apps/docs/scripts/search/sources/partner-integrations.ts b/apps/docs/scripts/search/sources/partner-integrations.ts index 8370d414d09..ad9964722e4 100644 --- a/apps/docs/scripts/search/sources/partner-integrations.ts +++ b/apps/docs/scripts/search/sources/partner-integrations.ts @@ -57,8 +57,8 @@ export class IntegrationSource extends BaseSource { super(source, path) } - process() { - const { checksum, sections } = processMdx(this.partnerData.overview) + async process() { + const { checksum, sections } = await processMdx(this.partnerData.overview) const meta = { title: upperFirst(this.partnerData.slug), subtitle: 'Integration', diff --git a/apps/docs/scripts/search/sources/reference-doc.ts b/apps/docs/scripts/search/sources/reference-doc.ts index c8c54211fa7..68a28e5e418 100644 --- a/apps/docs/scripts/search/sources/reference-doc.ts +++ b/apps/docs/scripts/search/sources/reference-doc.ts @@ -12,7 +12,6 @@ import { getApiEndpointById } from '../../../features/docs/Reference.generated.s import type { CliCommand, CliSpec } from '../../../generator/types/CliSpec.js' import { flattenSections } from '../../../lib/helpers.js' import { enrichedOperation, gen_v3 } from '../../../lib/refGenerator/helpers.js' -import type { Json } from '../../helpers.mdx.js' import { BaseLoader, BaseSource } from './base.js' export abstract class ReferenceLoader extends BaseLoader { @@ -24,7 +23,7 @@ export abstract class ReferenceLoader extends BaseLoader { constructor( source: string, path: string, - public meta: Json, + public meta: Record, public specFilePath: string, public sectionsFilePath: string ) { @@ -68,7 +67,7 @@ export abstract class ReferenceLoader extends BaseLoader { specSections: SpecSection[], id: string ): SpecSection | undefined | Promise - enhanceMeta(_section: SpecSection): Json { + enhanceMeta(_section: SpecSection): Record { return this.meta } } @@ -81,12 +80,12 @@ export abstract class ReferenceSource extends BaseSource { path: string, public refSection: ICommonSection, public specSection: SpecSection, - public meta: Json + public meta: Record ) { super(source, path) } - process() { + async process() { const checksum = createHash('sha256') .update(JSON.stringify(this.refSection) + JSON.stringify(this.specSection)) .digest('base64') @@ -125,7 +124,7 @@ export class OpenApiReferenceLoader extends ReferenceLoader, specFilePath: string, sectionsFilePath: string ) { @@ -251,7 +250,7 @@ export class ClientLibReferenceLoader extends ReferenceLoader, specFilePath: string, sectionsFilePath: string ) { @@ -272,7 +271,7 @@ export class ClientLibReferenceLoader extends ReferenceLoader functionDefinition.id === id) } - enhanceMeta(section: IFunctionDefinition): Json { + enhanceMeta(section: IFunctionDefinition): Record { return { ...this.meta, slug: section.id, methodName: section.title } } } @@ -311,7 +310,7 @@ export class CliReferenceLoader extends ReferenceLoader { constructor( source: string, path: string, - meta: Json, + meta: Record, specFilePath: string, sectionsFilePath: string ) { diff --git a/apps/docs/spec/cli_v1_commands.yaml b/apps/docs/spec/cli_v1_commands.yaml index d799f35c9ce..e5f3358609f 100644 --- a/apps/docs/spec/cli_v1_commands.yaml +++ b/apps/docs/spec/cli_v1_commands.yaml @@ -1,7 +1,7 @@ clispec: '001' info: id: cli - version: 2.33.9 + version: 2.34.3 title: Supabase CLI language: sh source: https://github.com/supabase/cli @@ -74,7 +74,7 @@ flags: name: --workdir description: path to a Supabase project directory default_value: '' - - id: 'yes' + - id: "yes" name: --yes description: answer yes to all prompts default_value: 'false' @@ -1003,7 +1003,7 @@ commands: summary: Delete a Supabase project tags: [] links: [] - usage: supabase projects delete + usage: supabase projects delete [ref] subcommands: [] flags: [] - id: supabase-projects-create @@ -3353,9 +3353,10 @@ commands: subcommands: - supabase-branches-create - supabase-branches-delete - - supabase-branches-disable - supabase-branches-get - supabase-branches-list + - supabase-branches-pause + - supabase-branches-unpause - supabase-branches-update flags: [] - id: supabase-branches-update @@ -3364,7 +3365,7 @@ commands: description: Update a preview branch by its name or ID. tags: [] links: [] - usage: supabase branches update [branch-id] [flags] + usage: supabase branches update [name] [flags] subcommands: [] flags: - id: git-branch @@ -3399,11 +3400,30 @@ commands: - id: FUNCTIONS_FAILED name: FUNCTIONS_FAILED type: string - - id: experimental - name: --experimental - description: enable experimental features - required: true - default_value: 'false' + - id: project-ref + name: --project-ref + description: Project ref of the Supabase project. + default_value: '' + - id: supabase-branches-unpause + title: supabase branches unpause + summary: Unpause a preview branch + tags: [] + links: [] + usage: supabase branches unpause [name] + subcommands: [] + flags: + - id: project-ref + name: --project-ref + description: Project ref of the Supabase project. + default_value: '' + - id: supabase-branches-pause + title: supabase branches pause + summary: Pause a preview branch + tags: [] + links: [] + usage: supabase branches pause [name] + subcommands: [] + flags: - id: project-ref name: --project-ref description: Project ref of the Supabase project. @@ -3417,11 +3437,6 @@ commands: usage: supabase branches list subcommands: [] flags: - - id: experimental - name: --experimental - description: enable experimental features - required: true - default_value: 'false' - id: project-ref name: --project-ref description: Project ref of the Supabase project. @@ -3432,32 +3447,9 @@ commands: description: Retrieve details of the specified preview branch. tags: [] links: [] - usage: supabase branches get [branch-id] + usage: supabase branches get [name] subcommands: [] flags: - - id: experimental - name: --experimental - description: enable experimental features - required: true - default_value: 'false' - - id: project-ref - name: --project-ref - description: Project ref of the Supabase project. - default_value: '' - - id: supabase-branches-disable - title: supabase branches disable - summary: Disable preview branching - description: Disable preview branching for the linked project. - tags: [] - links: [] - usage: supabase branches disable - subcommands: [] - flags: - - id: experimental - name: --experimental - description: enable experimental features - required: true - default_value: 'false' - id: project-ref name: --project-ref description: Project ref of the Supabase project. @@ -3468,14 +3460,9 @@ commands: description: Delete a preview branch by its name or ID. tags: [] links: [] - usage: supabase branches delete [branch-id] + usage: supabase branches delete [name] subcommands: [] flags: - - id: experimental - name: --experimental - description: enable experimental features - required: true - default_value: 'false' - id: project-ref name: --project-ref description: Project ref of the Supabase project. @@ -3606,11 +3593,6 @@ commands: name: --with-data description: Whether to clone production data to the branch database. default_value: 'false' - - id: experimental - name: --experimental - description: enable experimental features - required: true - default_value: 'false' - id: project-ref name: --project-ref description: Project ref of the Supabase project. diff --git a/apps/docs/spec/supabase_dart_v2.yml b/apps/docs/spec/supabase_dart_v2.yml index 8bfbbc93ecd..82e4ab3e1ce 100644 --- a/apps/docs/spec/supabase_dart_v2.yml +++ b/apps/docs/spec/supabase_dart_v2.yml @@ -859,7 +859,7 @@ functions: - id: verify-otp title: 'verifyOtp()' notes: | - - The `verifyOtp` method takes in different verification types. If a phone number is used, the type can either be `sms` or `phone_change`. If an email address is used, the type can be one of the following: `signup`, `magiclink`, `recovery`, `invite` or `email_change`. + - The `verifyOtp` method takes in different verification types. If a phone number is used, the type can either be `sms` or `phone_change`. If an email address is used, the type can be one of the following: `email`, `recovery`, `invite` or `email_change` (`signup` and `magiclink` types are deprecated). - The verification type used should be determined based on the corresponding auth method called before `verifyOtp` to sign up or sign in a user. params: - name: token @@ -1633,9 +1633,9 @@ functions: notes: | This section contains methods commonly used for Multi-Factor Authentication (MFA) and are invoked behind the `supabase.auth.mfa` namespace. - Currently, we only support time-based one-time password (TOTP) as the 2nd factor. We don't support recovery codes but we allow users to enroll more than 1 TOTP factor, with an upper limit of 10. + Currently, Supabase supports time-based one-time password (TOTP) and phone verification code as the 2nd factor. Recovery codes are not supported but users can enroll multiple factors, with an upper limit of 10.. - Having a 2nd TOTP factor for recovery means the user doesn't have to store their recovery codes. It also reduces the attack surface since the recovery factor is usually time-limited and not a single static code. + Having a 2nd factor for recovery frees the user of the burden of having to store their recovery codes somewhere. It also reduces the attack surface since multiple recovery codes are usually generated compared to just having 1 backup factor. Learn more about implementing MFA on your application on our guide [here](https://supabase.com/docs/guides/auth/auth-mfa#overview). - id: mfa-enroll @@ -1644,7 +1644,7 @@ functions: Starts the enrollment process for a new Multi-Factor Authentication (MFA) factor. This method creates a new `unverified` factor. To verify a factor, present the QR code or secret to the user and ask them to add it to their authenticator app. The user has to enter the code from their authenticator app to verify it. - - Currently, `totp` is the only supported `factorType`. The returned `id` should be used to create a challenge. + - Use `totp` or `phone` as the `factorType` and the returned `id` to create a challenge. - To create a challenge, see [`mfa.challenge()`](/docs/reference/dart/auth-mfa-challenge). - To verify a challenge, see [`mfa.verify()`](/docs/reference/dart/auth-mfa-verify). - To create and verify a challenge in a single step, see [`mfa.challengeAndVerify()`](/docs/reference/dart/auth-mfa-challengeandverify). @@ -1661,6 +1661,10 @@ functions: isOptional: true type: String description: Human readable name assigned to the factor. + - name: phone + isOptional: true + type: String + description: Phone number to enroll for phone factor type. examples: - id: enroll-totp-factor name: Enroll a time-based, one-time password (TOTP) factor @@ -1681,6 +1685,27 @@ functions: secret: '', uri: '', ), + phone: null, + ); + ``` + - id: enroll-phone-factor + name: Enroll a Phone Factor + isSpotlight: true + code: | + ```dart + final res = await supabase.auth.mfa.enroll(factorType: FactorType.phone, phone: '+1234567890'); + + final phone = res.phone; + ``` + response: | + ```json + AuthMFAEnrollResponse( + id: '', + type: FactorType.phone, + totp: null, + phone: PhoneEnrollment( + phone: '+1234567890', + ), ); ``` - id: mfa-challenge diff --git a/apps/docs/spec/supabase_js_v2.yml b/apps/docs/spec/supabase_js_v2.yml index a9834a43420..6eb80d4b262 100644 --- a/apps/docs/spec/supabase_js_v2.yml +++ b/apps/docs/spec/supabase_js_v2.yml @@ -2128,6 +2128,8 @@ functions: Currently, there is support for time-based one-time password (TOTP) and phone verification code as the 2nd factor. Recovery codes are not supported but users can enroll multiple factors, with an upper limit of 10. Having a 2nd factor for recovery frees the user of the burden of having to store their recovery codes somewhere. It also reduces the attack surface since multiple recovery codes are usually generated compared to just having 1 backup factor. + + Learn more about implementing MFA in your application [in the MFA guide](https://supabase.com/docs/guides/auth/auth-mfa#overview). - id: mfa-enroll title: 'mfa.enroll()' $ref: '@supabase/auth-js.GoTrueMFAApi.enroll' diff --git a/apps/docs/spec/supabase_py_v2.yml b/apps/docs/spec/supabase_py_v2.yml index a19afdbd019..661cf1e884e 100644 --- a/apps/docs/spec/supabase_py_v2.yml +++ b/apps/docs/spec/supabase_py_v2.yml @@ -925,6 +925,74 @@ functions: {"provider_id": "21648a9d-8d5a-4555-a9d1-d6375dc14e92"} ) ``` + - id: get-claims + title: 'get_claims' + params: + - name: jwt + isOptional: true + type: string + description: An optional specific JWT you wish to verify, not the one you can obtain from [`get_session`](/docs/reference/python/auth-getsession). + - name: jwks + isOptional: true + type: JWKSet + description: If set, this JSON Web Key Set is going to have precedence over the cached value available on the server. + subContent: + - name: keys + isOptional: false + type: list + subContent: + - name: key_opt + isOptional: false + type: list[string] + - name: kty + isOptional: false + type: RSA | EC | oct + - name: alg + isOptional: false + type: string + - name: kid + isOptional: false + type: string + notes: | + Extracts the JWT claims present in the access token by first verifying the JWT against the server's JSON Web Key Set endpoint /.well-known/jwks.json which is often cached, resulting in significantly faster responses. Prefer this method over [`get_user`](/docs/reference/python/auth-getuser) which always sends a request to the Auth server for each JWT. + examples: + - id: get-user-object + name: Get user object + isSpotlight: true + code: | + ``` + response = supabase.auth.get_claims() + ``` + response: | + ```json + { + "claims": { + "aal": "aal1", + "amr": [{ + "method": "email", + "timestamp": 1715766000 + }], + "app_metadata": {}, + "aud": "authenticated", + "email": "example@email.com", + "exp": 1715769600, + "iat": 1715766000, + "is_anonymous": false, + "iss": "https://api.supabase.com/auth/v1", + "phone": "+13334445555", + "role": "authenticated", + "session_id": "11111111-1111-1111-1111-111111111111", + "sub": "11111111-1111-1111-1111-111111111111", + "user_metadata": {} + }, + "header": { + "alg": "RS256", + "typ": "JWT", + "kid": "abcdefgh" + }, + "signature": [/** Uint8Array */], + } + ``` - id: sign-out title: 'sign_out()' params: diff --git a/apps/docs/spec/supabase_swift_v1.yml b/apps/docs/spec/supabase_swift_v1.yml index e88c32c51ec..f8d4b851d78 100644 --- a/apps/docs/spec/supabase_swift_v1.yml +++ b/apps/docs/spec/supabase_swift_v1.yml @@ -25,13 +25,13 @@ functions: name: Initialize Client code: | ```swift - let client = SupabaseClient(supabaseURL: URL(string: "https://xyzcompany.supabase.co")!, supabaseKey: "public-anon-key") + let supabase = SupabaseClient(supabaseURL: URL(string: "https://xyzcompany.supabase.co")!, supabaseKey: "public-anon-key") ``` - id: initialize-client-custom-options name: Initialize Client with custom options code: | ```swift - let client = SupabaseClient( + let supabase = SupabaseClient( supabaseURL: URL(string: "https://xyzcompany.supabase.co")!, supabaseKey: "public-anon-key", options: SupabaseClientOptions( diff --git a/apps/docs/spec/supabase_swift_v2.yml b/apps/docs/spec/supabase_swift_v2.yml index 4c83fb45c73..64c83d73331 100644 --- a/apps/docs/spec/supabase_swift_v2.yml +++ b/apps/docs/spec/supabase_swift_v2.yml @@ -27,7 +27,7 @@ functions: ```swift import Supabase - let client = SupabaseClient(supabaseURL: URL(string: "https://xyzcompany.supabase.co")!, supabaseKey: "public-anon-key") + let supabase = SupabaseClient(supabaseURL: URL(string: "https://xyzcompany.supabase.co")!, supabaseKey: "public-anon-key") ``` - id: initialize-client-custom-options name: Initialize Client with custom options @@ -35,7 +35,7 @@ functions: ```swift import Supabase - let client = SupabaseClient( + let supabase = SupabaseClient( supabaseURL: URL(string: "https://xyzcompany.supabase.co")!, supabaseKey: "public-anon-key", options: SupabaseClientOptions( @@ -65,7 +65,7 @@ functions: } } - let client = SupabaseClient( + let supabase = SupabaseClient( supabaseURL: URL(string: "https://xyzcompany.supabase.co")!, supabaseKey: "public-anon-key", options: SupabaseClientOptions( diff --git a/apps/studio/components/grid/components/editor/BooleanEditor.tsx b/apps/studio/components/grid/components/editor/BooleanEditor.tsx index bb8f0d3d2d4..39953d1c493 100644 --- a/apps/studio/components/grid/components/editor/BooleanEditor.tsx +++ b/apps/studio/components/grid/components/editor/BooleanEditor.tsx @@ -1,5 +1,4 @@ import type { RenderEditCellProps } from 'react-data-grid' -import { useTableEditorTableStateSnapshot } from 'state/table-editor-table' import { Select } from 'ui' interface Props extends RenderEditCellProps { @@ -13,8 +12,6 @@ export const BooleanEditor = ({ onRowChange, onClose, }: Props) => { - const snap = useTableEditorTableStateSnapshot() - const gridColumn = snap.gridColumns.find((x) => x.name == column.key) const value = row[column.key as keyof TRow] as unknown as string const onBlur = () => onClose(false) @@ -36,7 +33,7 @@ export const BooleanEditor = ({ onBlur={onBlur} onChange={onChange} defaultValue={value === null ? 'null' : value.toString()} - style={{ width: `${gridColumn?.width || column.width}px` }} + style={{ width: `${column.width}px` }} > TRUE FALSE diff --git a/apps/studio/components/grid/components/editor/JsonEditor.tsx b/apps/studio/components/grid/components/editor/JsonEditor.tsx index d1c441c0bc5..e99c1104c6c 100644 --- a/apps/studio/components/grid/components/editor/JsonEditor.tsx +++ b/apps/studio/components/grid/components/editor/JsonEditor.tsx @@ -10,7 +10,6 @@ import { isTableLike } from 'data/table-editor/table-editor-types' import { useGetCellValueMutation } from 'data/table-rows/get-cell-value-mutation' import { useSelectedProjectQuery } from 'hooks/misc/useSelectedProject' import { prettifyJSON, removeJSONTrailingComma, tryParseJson } from 'lib/helpers' -import { useTableEditorTableStateSnapshot } from 'state/table-editor-table' import { Popover, Tooltip, TooltipContent, TooltipTrigger } from 'ui' import { BlockKeys } from '../common/BlockKeys' import { MonacoEditor } from '../common/MonacoEditor' @@ -55,7 +54,6 @@ export const JsonEditor = ({ const { id: _id } = useParams() const id = _id ? Number(_id) : undefined const { data: project } = useSelectedProjectQuery() - const snap = useTableEditorTableStateSnapshot() const { data: selectedTable } = useTableEditorQuery({ projectRef: project?.ref, @@ -63,8 +61,6 @@ export const JsonEditor = ({ id, }) - const gridColumn = snap.gridColumns.find((x) => x.name == column.key) - const rawInitialValue = row[column.key as keyof TRow] as unknown const initialValue = rawInitialValue === null || rawInitialValue === undefined || typeof rawInitialValue === 'string' @@ -161,13 +157,13 @@ export const JsonEditor = ({ overlay={ isTruncated && !isSuccess ? (
{}} - width={`${gridColumn?.width || column.width}px`} + width={`${column.width}px`} value={value ?? ''} language="markdown" /> @@ -176,7 +172,7 @@ export const JsonEditor = ({ ) : ( extends RenderEditCellProps { isNullable?: boolean @@ -17,9 +15,6 @@ export function SelectEditor({ options, isNullable, }: SelectEditorProps) { - const snap = useTableEditorTableStateSnapshot() - const gridColumn = snap.gridColumns.find((x) => x.name == column.key) - const value = row[column.key as keyof TRow] as unknown as string function onChange(event: any) { @@ -42,7 +37,7 @@ export function SelectEditor({ size="small" defaultValue={value ?? ''} className="sb-grid-select-editor !gap-2" - style={{ width: `${gridColumn?.width || column.width}px` }} + style={{ width: `${column.width}px` }} // @ts-ignore onChange={onChange} onBlur={onBlur} diff --git a/apps/studio/components/grid/components/editor/TextEditor.tsx b/apps/studio/components/grid/components/editor/TextEditor.tsx index 15e111e13fd..ca49402a3d5 100644 --- a/apps/studio/components/grid/components/editor/TextEditor.tsx +++ b/apps/studio/components/grid/components/editor/TextEditor.tsx @@ -9,7 +9,6 @@ import { useTableEditorQuery } from 'data/table-editor/table-editor-query' import { isTableLike } from 'data/table-editor/table-editor-types' import { useGetCellValueMutation } from 'data/table-rows/get-cell-value-mutation' import { useSelectedProjectQuery } from 'hooks/misc/useSelectedProject' -import { useTableEditorTableStateSnapshot } from 'state/table-editor-table' import { Button, Popover, Tooltip, TooltipContent, TooltipTrigger, cn } from 'ui' import ConfirmationModal from 'ui-patterns/Dialogs/ConfirmationModal' import { BlockKeys } from '../common/BlockKeys' @@ -30,7 +29,6 @@ export const TextEditor = ({ isEditable?: boolean onExpandEditor: (column: string, row: TRow) => void }) => { - const snap = useTableEditorTableStateSnapshot() const { id: _id } = useParams() const id = _id ? Number(_id) : undefined const { data: project } = useSelectedProjectQuery() @@ -41,7 +39,6 @@ export const TextEditor = ({ id, }) - const gridColumn = snap.gridColumns.find((x) => x.name == column.key) const rawValue = row[column.key as keyof TRow] as unknown const initialValue = rawValue || rawValue === '' ? String(rawValue) : null const [isPopoverOpen, setIsPopoverOpen] = useState(true) @@ -114,13 +111,13 @@ export const TextEditor = ({ overlay={ isTruncated && !isSuccess ? (
{}} - width={`${gridColumn?.width || column.width}px`} + width={`${column.width}px`} value={value ?? ''} language="markdown" /> @@ -134,7 +131,7 @@ export const TextEditor = ({ ignoreOutsideClicks={isConfirmNextModalOpen} > tableEditorSnap.onImportData(valtioRef(file)), + onTelemetryEvent: (eventName) => { + sendEvent({ + action: eventName, + groups: { + project: project?.ref ?? 'Unknown', + organization: org?.slug ?? 'Unknown', + }, + }) + }, + }) + const { data } = useForeignKeyConstraintsQuery({ projectRef: project?.ref, connectionString: project?.connectionString, @@ -115,8 +134,15 @@ export const Grid = memo( return (
{/* Render no rows fallback outside of the DataGrid */} {(rows ?? []).length === 0 && ( @@ -143,7 +169,9 @@ export const Grid = memo( <> {(filters ?? []).length === 0 ? (
-

This table is empty

+

+ {isDraggedOver ? 'Drop your CSV file here' : 'This table is empty'} +

{tableEntityType === ENTITY_TYPE.FOREIGN_TABLE ? (

@@ -152,25 +180,30 @@ export const Grid = memo(

) : ( -
- -
+ !isDraggedOver && ( +
+ +

+ or drag and drop a CSV file here +

+
+ ) )}
) : ( diff --git a/apps/studio/components/grid/components/header/Header.tsx b/apps/studio/components/grid/components/header/Header.tsx index 2b2429cd333..fde164a247c 100644 --- a/apps/studio/components/grid/components/header/Header.tsx +++ b/apps/studio/components/grid/components/header/Header.tsx @@ -2,7 +2,6 @@ import { PermissionAction } from '@supabase/shared-types/out/constants' import saveAs from 'file-saver' import { ArrowUp, ChevronDown, FileText, Trash } from 'lucide-react' import Link from 'next/link' -import Papa from 'papaparse' import { ReactNode, useState } from 'react' import { toast } from 'sonner' @@ -28,6 +27,7 @@ import { useTableEditorTableStateSnapshot } from 'state/table-editor-table' import { Button, cn, + copyToClipboard, DropdownMenu, DropdownMenuContent, DropdownMenuItem, @@ -37,6 +37,7 @@ import { } from 'ui' import { ExportDialog } from './ExportDialog' import { FilterPopover } from './filter/FilterPopover' +import { formatRowsForCSV } from './Header.utils' import { SortPopover } from './sort/SortPopover' // [Joshen] CSV exports require this guard as a fail-safe if the table is // just too large for a browser to keep all the rows in memory before @@ -281,6 +282,25 @@ const RowHeader = () => { }) } + const onCopyRows = (type: 'csv' | 'json' | 'sql') => { + const rows = allRows.filter((x) => snap.selectedRows.has(x.idx)) + + if (type === 'csv') { + const csv = formatRowsForCSV({ + rows, + columns: snap.table!.columns.map((column) => column.name), + }) + copyToClipboard(csv) + } else if (type === 'sql') { + const sqlStatements = formatTableRowsToSQL(snap.table, rows) + copyToClipboard(sqlStatements) + } else if (type === 'json') { + copyToClipboard(JSON.stringify(rows)) + } + + toast.success('Copied rows to clipboard') + } + const onRowsExportCSV = async () => { setIsExporting(true) @@ -340,16 +360,8 @@ const RowHeader = () => { return } - const formattedRows = rows.map((row) => { - const formattedRow = row - Object.keys(row).map((column) => { - if (typeof row[column] === 'object' && row[column] !== null) - formattedRow[column] = JSON.stringify(formattedRow[column]) - }) - return formattedRow - }) - - const csv = Papa.unparse(formattedRows, { + const csv = formatRowsForCSV({ + rows, columns: snap.table!.columns.map((column) => column.name), }) const csvData = new Blob([csv], { type: 'text/csv;charset=utf-8;' }) @@ -494,6 +506,42 @@ const RowHeader = () => { : `Delete ${snap.selectedRows.size} row`} )} + + {!snap.allRowsSelected ? ( + + + + + + onCopyRows('csv')}>Copy as CSV + onCopyRows('sql')}>Copy as SQL + onCopyRows('json')}>Copy as JSON + + + ) : ( + + Copy + + )} +
+ } + arrow={} + > - - Copy cell content + Copy cell -
- - {isOn && ( -
-

- Installed in {extension.schema} schema -

-
- )} - -
-

{extension.comment}

-
- {extensionMeta?.github_url && ( - - )} - {docsUrl !== undefined && ( - - )} - {extensionMeta?.deprecated && extensionMeta?.deprecated.length > 0 && ( - } - className="rounded-full" - tooltip={{ - content: { - text: `The extension is deprecated and will be removed in ${extensionMeta.deprecated.join(', ')}.`, - }, - }} - > - Deprecated - - )} -
-
- - {extensionMeta?.product && ( -
-
- -
-
-

- {extension.name} is used by{' '} - {extensionMeta.product_url ? ( - - {extensionMeta.product} - - ) : ( - extensionMeta.product - )} -

- {!isOn && ( -

- Install extension to use {extensionMeta.product} -

- )} -
-
- )} -
+ + { ) } -export default ExtensionCard +export default ExtensionRow diff --git a/apps/studio/components/interfaces/Database/Extensions/Extensions.tsx b/apps/studio/components/interfaces/Database/Extensions/Extensions.tsx index 73ca129d0c5..4aba0c0fa61 100644 --- a/apps/studio/components/interfaces/Database/Extensions/Extensions.tsx +++ b/apps/studio/components/interfaces/Database/Extensions/Extensions.tsx @@ -7,13 +7,12 @@ import { useParams } from 'common' import { DocsButton } from 'components/ui/DocsButton' import InformationBox from 'components/ui/InformationBox' import NoSearchResults from 'components/ui/NoSearchResults' -import ShimmeringLoader from 'components/ui/ShimmeringLoader' +import { GenericSkeletonLoader } from 'components/ui/ShimmeringLoader' import { useDatabaseExtensionsQuery } from 'data/database-extensions/database-extensions-query' -import { useCheckPermissions, usePermissionsLoaded } from 'hooks/misc/useCheckPermissions' +import { useAsyncCheckProjectPermissions } from 'hooks/misc/useCheckPermissions' import { useSelectedProjectQuery } from 'hooks/misc/useSelectedProject' -import { Input } from 'ui' -import ExtensionCard from './ExtensionCard' -import ExtensionCardSkeleton from './ExtensionCardSkeleton' +import { Card, Input, Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from 'ui' +import ExtensionRow from './ExtensionRow' import { HIDDEN_EXTENSIONS, SEARCH_TERMS } from './Extensions.constants' const Extensions = () => { @@ -42,11 +41,8 @@ const Extensions = () => { (ext) => !isNull(ext.installed_version) ) - const canUpdateExtensions = useCheckPermissions( - PermissionAction.TENANT_SQL_ADMIN_WRITE, - 'extensions' - ) - const isPermissionsLoaded = usePermissionsLoaded() + const { can: canUpdateExtensions, isSuccess: isPermissionsLoaded } = + useAsyncCheckProjectPermissions(PermissionAction.TENANT_SQL_ADMIN_WRITE, 'extensions') useEffect(() => { if (filter !== undefined) setFilterString(filter as string) @@ -76,50 +72,46 @@ const Extensions = () => { )} {isLoading ? ( -
-
- - -
- {Array.from({ length: 6 }).map((_, index) => ( - - ))} -
-
-
+ ) : ( - <> - {extensions.length === 0 && ( - setFilterString('')} - /> - )} - -
- {enabledExtensions.length > 0 && ( -
-

Enabled extensions

-
- {enabledExtensions.map((extension) => ( - - ))} -
-
- )} - - {disabledExtensions.length > 0 && ( -
-

Available extensions

-
- {disabledExtensions.map((extension) => ( - - ))} -
-
- )} -
- +
+ + + + + Name + Version + Schema + Description + Used by + Links + + Enabled + + + + + {[...enabledExtensions, ...disabledExtensions].map((extension) => ( + + ))} + {extensions.length === 0 && ( + + + setFilterString('')} + /> + + + )} + +
+
+
)} ) diff --git a/apps/studio/components/interfaces/Database/Functions/FunctionsList/FunctionList.tsx b/apps/studio/components/interfaces/Database/Functions/FunctionsList/FunctionList.tsx index 9e42241f27c..680755b6c32 100644 --- a/apps/studio/components/interfaces/Database/Functions/FunctionsList/FunctionList.tsx +++ b/apps/studio/components/interfaces/Database/Functions/FunctionsList/FunctionList.tsx @@ -3,10 +3,9 @@ import { includes, noop, sortBy } from 'lodash' import { Edit, Edit2, FileText, MoreVertical, Trash } from 'lucide-react' import { useRouter } from 'next/router' -import Table from 'components/to-be-cleaned/Table' import { ButtonTooltip } from 'components/ui/ButtonTooltip' import { useDatabaseFunctionsQuery } from 'data/database-functions/database-functions-query' -import { useCheckPermissions } from 'hooks/misc/useCheckPermissions' +import { useAsyncCheckProjectPermissions } from 'hooks/misc/useCheckPermissions' import { useSelectedProjectQuery } from 'hooks/misc/useSelectedProject' import { useAiAssistantStateSnapshot } from 'state/ai-assistant-state' import { @@ -16,6 +15,8 @@ import { DropdownMenuItem, DropdownMenuSeparator, DropdownMenuTrigger, + TableRow, + TableCell, } from 'ui' interface FunctionListProps { @@ -50,34 +51,34 @@ const FunctionList = ({ (func) => func.name.toLocaleLowerCase() ) const projectRef = selectedProject?.ref - const canUpdateFunctions = useCheckPermissions( + const { can: canUpdateFunctions } = useAsyncCheckProjectPermissions( PermissionAction.TENANT_SQL_ADMIN_WRITE, 'functions' ) if (_functions.length === 0 && filterString.length === 0) { return ( - - + +

No functions created yet

There are no functions found in the schema "{schema}"

-
-
+ + ) } if (_functions.length === 0 && filterString.length > 0) { return ( - - + +

No results found

Your search for "{filterString}" did not return any results

-
-
+ + ) } @@ -87,8 +88,8 @@ const FunctionList = ({ const isApiDocumentAvailable = schema == 'public' && x.return_type !== 'trigger' return ( - - + + - - + +

{x.argument_types || '-'}

-
- + +

{x.return_type}

-
- {x.security_definer ? 'Definer' : 'Invoker'} - + + + {x.security_definer ? 'Definer' : 'Invoker'} + + {!isLocked && (
{canUpdateFunctions ? ( @@ -189,8 +192,8 @@ const FunctionList = ({ )}
)} -
-
+ + ) })} diff --git a/apps/studio/components/interfaces/Database/Functions/FunctionsList/FunctionsList.tsx b/apps/studio/components/interfaces/Database/Functions/FunctionsList/FunctionsList.tsx index 0af82c75f81..bc8f73b826a 100644 --- a/apps/studio/components/interfaces/Database/Functions/FunctionsList/FunctionsList.tsx +++ b/apps/studio/components/interfaces/Database/Functions/FunctionsList/FunctionsList.tsx @@ -6,19 +6,28 @@ import { useRouter } from 'next/router' import { useParams } from 'common' import ProductEmptyState from 'components/to-be-cleaned/ProductEmptyState' -import Table from 'components/to-be-cleaned/Table' import AlertError from 'components/ui/AlertError' import { ButtonTooltip } from 'components/ui/ButtonTooltip' import SchemaSelector from 'components/ui/SchemaSelector' import { GenericSkeletonLoader } from 'components/ui/ShimmeringLoader' import { useDatabaseFunctionsQuery } from 'data/database-functions/database-functions-query' import { useSchemasQuery } from 'data/database/schemas-query' -import { useCheckPermissions } from 'hooks/misc/useCheckPermissions' +import { useAsyncCheckProjectPermissions } from 'hooks/misc/useCheckPermissions' import { useQuerySchemaState } from 'hooks/misc/useSchemaQueryState' import { useSelectedProjectQuery } from 'hooks/misc/useSelectedProject' import { useIsProtectedSchema } from 'hooks/useProtectedSchemas' import { useAiAssistantStateSnapshot } from 'state/ai-assistant-state' -import { AiIconAnimation, Input } from 'ui' +import { + AiIconAnimation, + Input, + Table, + TableHeader, + TableHead, + TableBody, + TableRow, + TableCell, + Card, +} from 'ui' import { ProtectedSchemaWarning } from '../../ProtectedSchemaWarning' import FunctionList from './FunctionList' @@ -51,18 +60,19 @@ const FunctionsList = ({ router.push(url) } - const canCreateFunctions = useCheckPermissions( + const { can: canCreateFunctions } = useAsyncCheckProjectPermissions( PermissionAction.TENANT_SQL_ADMIN_WRITE, 'functions' ) - const { data: schemas } = useSchemasQuery({ + const { isSchemaLocked } = useIsProtectedSchema({ schema: selectedSchema }) + + // [Joshen] This is to preload the data for the Schema Selector + useSchemasQuery({ projectRef: project?.ref, connectionString: project?.connectionString, }) - const { isSchemaLocked } = useIsProtectedSchema({ schema: selectedSchema }) - const { data: functions, error, @@ -167,34 +177,34 @@ const FunctionsList = ({
{isSchemaLocked && } - - - Name - - Arguments - - - Return type - - - Security - - - - } - body={ - - } - /> + +
+ + + Name + + Arguments + + + Return type + + + Security + + + + + + + +
+
)} diff --git a/apps/studio/components/interfaces/Database/Hooks/HooksList/HookList.tsx b/apps/studio/components/interfaces/Database/Hooks/HooksList/HookList.tsx index 4f37d899979..c259acb39d0 100644 --- a/apps/studio/components/interfaces/Database/Hooks/HooksList/HookList.tsx +++ b/apps/studio/components/interfaces/Database/Hooks/HooksList/HookList.tsx @@ -1,3 +1,4 @@ +import { PostgresTrigger } from '@supabase/postgres-meta' import { PermissionAction } from '@supabase/shared-types/out/constants' import { includes, noop } from 'lodash' import { Edit3, MoreVertical, Trash } from 'lucide-react' @@ -7,7 +8,7 @@ import { useParams } from 'common' import Table from 'components/to-be-cleaned/Table' import { ButtonTooltip } from 'components/ui/ButtonTooltip' import { useDatabaseHooksQuery } from 'data/database-triggers/database-triggers-query' -import { useCheckPermissions } from 'hooks/misc/useCheckPermissions' +import { useAsyncCheckProjectPermissions } from 'hooks/misc/useCheckPermissions' import { useSelectedProjectQuery } from 'hooks/misc/useSelectedProject' import { BASE_PATH } from 'lib/constants' import { @@ -23,11 +24,16 @@ import { export interface HookListProps { schema: string filterString: string - editHook: (hook: any) => void - deleteHook: (hook: any) => void + editHook: (hook: PostgresTrigger) => void + deleteHook: (hook: PostgresTrigger) => void } -const HookList = ({ schema, filterString, editHook = noop, deleteHook = noop }: HookListProps) => { +export const HookList = ({ + schema, + filterString, + editHook = noop, + deleteHook = noop, +}: HookListProps) => { const { ref } = useParams() const { data: project } = useSelectedProjectQuery() const { data: hooks } = useDatabaseHooksQuery({ @@ -39,16 +45,19 @@ const HookList = ({ schema, filterString, editHook = noop, deleteHook = noop }: const restUrlTld = restUrl ? new URL(restUrl).hostname.split('.').pop() : 'co' const filteredHooks = (hooks ?? []).filter( - (x: any) => + (x) => includes(x.name.toLowerCase(), filterString.toLowerCase()) && x.schema === schema && x.function_args.length >= 2 ) - const canUpdateWebhook = useCheckPermissions(PermissionAction.TENANT_SQL_ADMIN_WRITE, 'triggers') + const { can: canUpdateWebhook } = useAsyncCheckProjectPermissions( + PermissionAction.TENANT_SQL_ADMIN_WRITE, + 'triggers' + ) return ( <> - {filteredHooks.map((x: any) => { + {filteredHooks.map((x) => { const isEdgeFunction = (url: string) => url.includes(`https://${ref}.functions.supabase.${restUrlTld}/`) || url.includes(`https://${ref}.supabase.${restUrlTld}/functions/`) @@ -134,5 +143,3 @@ const HookList = ({ schema, filterString, editHook = noop, deleteHook = noop }: ) } - -export default HookList diff --git a/apps/studio/components/interfaces/Database/Hooks/HooksList/HooksList.tsx b/apps/studio/components/interfaces/Database/Hooks/HooksList/HooksList.tsx index e255efb78d0..7340883f745 100644 --- a/apps/studio/components/interfaces/Database/Hooks/HooksList/HooksList.tsx +++ b/apps/studio/components/interfaces/Database/Hooks/HooksList/HooksList.tsx @@ -1,3 +1,4 @@ +import { PostgresTrigger } from '@supabase/postgres-meta' import { PermissionAction } from '@supabase/shared-types/out/constants' import { includes, map as lodashMap, uniqBy } from 'lodash' import { Search } from 'lucide-react' @@ -9,20 +10,27 @@ import { DocsButton } from 'components/ui/DocsButton' import NoSearchResults from 'components/ui/NoSearchResults' import { GenericSkeletonLoader } from 'components/ui/ShimmeringLoader' import { useDatabaseHooksQuery } from 'data/database-triggers/database-triggers-query' -import { useCheckPermissions, usePermissionsLoaded } from 'hooks/misc/useCheckPermissions' +import { + useAsyncCheckProjectPermissions, + usePermissionsLoaded, +} from 'hooks/misc/useCheckPermissions' import { useSelectedProjectQuery } from 'hooks/misc/useSelectedProject' import { noop } from 'lib/void' import { Input } from 'ui' -import HooksListEmpty from './HooksListEmpty' -import SchemaTable from './SchemaTable' +import { HooksListEmpty } from './HooksListEmpty' +import { SchemaTable } from './SchemaTable' export interface HooksListProps { createHook: () => void - editHook: (hook: any) => void - deleteHook: (hook: any) => void + editHook: (hook: PostgresTrigger) => void + deleteHook: (hook: PostgresTrigger) => void } -const HooksList = ({ createHook = noop, editHook = noop, deleteHook = noop }: HooksListProps) => { +export const HooksList = ({ + createHook = noop, + editHook = noop, + deleteHook = noop, +}: HooksListProps) => { const { data: project } = useSelectedProjectQuery() const { data: hooks, @@ -41,7 +49,10 @@ const HooksList = ({ createHook = noop, editHook = noop, deleteHook = noop }: Ho ) const filteredHookSchemas = lodashMap(uniqBy(filteredHooks, 'schema'), 'schema') - const canCreateWebhooks = useCheckPermissions(PermissionAction.TENANT_SQL_ADMIN_WRITE, 'triggers') + const { can: canCreateWebhooks } = useAsyncCheckProjectPermissions( + PermissionAction.TENANT_SQL_ADMIN_WRITE, + 'triggers' + ) const isPermissionsLoaded = usePermissionsLoaded() return ( @@ -108,5 +119,3 @@ const HooksList = ({ createHook = noop, editHook = noop, deleteHook = noop }: Ho
) } - -export default HooksList diff --git a/apps/studio/components/interfaces/Database/Hooks/HooksList/HooksListEmpty.tsx b/apps/studio/components/interfaces/Database/Hooks/HooksList/HooksListEmpty.tsx index 2d466c0708b..0aa73a04c31 100644 --- a/apps/studio/components/interfaces/Database/Hooks/HooksList/HooksListEmpty.tsx +++ b/apps/studio/components/interfaces/Database/Hooks/HooksList/HooksListEmpty.tsx @@ -1,6 +1,6 @@ import Table from 'components/to-be-cleaned/Table' -const HooksListEmpty = () => { +export const HooksListEmpty = () => { return ( { /> ) } - -export default HooksListEmpty diff --git a/apps/studio/components/interfaces/Database/Hooks/HooksList/SchemaTable.tsx b/apps/studio/components/interfaces/Database/Hooks/HooksList/SchemaTable.tsx index 52e0f48afdc..0ebbc7dcd76 100644 --- a/apps/studio/components/interfaces/Database/Hooks/HooksList/SchemaTable.tsx +++ b/apps/studio/components/interfaces/Database/Hooks/HooksList/SchemaTable.tsx @@ -1,16 +1,17 @@ +import { PostgresTrigger } from '@supabase/postgres-meta' import { noop } from 'lodash' import Table from 'components/to-be-cleaned/Table' -import HookList from './HookList' +import { HookList } from './HookList' interface SchemaTableProps { schema: string filterString: string - editHook: (hook: any) => void - deleteHook: (hook: any) => void + editHook: (hook: PostgresTrigger) => void + deleteHook: (hook: PostgresTrigger) => void } -const SchemaTable = ({ +export const SchemaTable = ({ schema, filterString, editHook = noop, @@ -55,5 +56,3 @@ const SchemaTable = ({ ) } - -export default SchemaTable diff --git a/apps/studio/components/interfaces/Database/Indexes/Indexes.tsx b/apps/studio/components/interfaces/Database/Indexes/Indexes.tsx index 3fe2c0dff97..8cabd076c5c 100644 --- a/apps/studio/components/interfaces/Database/Indexes/Indexes.tsx +++ b/apps/studio/components/interfaces/Database/Indexes/Indexes.tsx @@ -4,7 +4,6 @@ import { useEffect, useState } from 'react' import { toast } from 'sonner' import { useParams } from 'common' -import Table from 'components/to-be-cleaned/Table' import AlertError from 'components/ui/AlertError' import CodeEditor from 'components/ui/CodeEditor/CodeEditor' import SchemaSelector from 'components/ui/SchemaSelector' @@ -15,7 +14,18 @@ import { useSchemasQuery } from 'data/database/schemas-query' import { useQuerySchemaState } from 'hooks/misc/useSchemaQueryState' import { useSelectedProjectQuery } from 'hooks/misc/useSelectedProject' import { useIsProtectedSchema } from 'hooks/useProtectedSchemas' -import { Button, Input, SidePanel } from 'ui' +import { + Button, + Input, + SidePanel, + Table, + TableBody, + TableCell, + TableHead, + TableRow, + TableHeader, + Card, +} from 'ui' import ConfirmationModal from 'ui-patterns/Dialogs/ConfirmationModal' import { ProtectedSchemaWarning } from '../ProtectedSchemaWarning' import CreateIndexSidePanel from './CreateIndexSidePanel' @@ -139,49 +149,51 @@ const Indexes = () => { )} {isSuccessIndexes && ( -
-
Schema, - Table, - Name, - , - ]} - body={ - <> +
+ +
+ + + Schema + Table + Name + + + + {sortedIndexes.length === 0 && search.length === 0 && ( - - + +

No indexes created yet

There are no indexes found in the schema "{selectedSchema}"

-
-
+ + )} {sortedIndexes.length === 0 && search.length > 0 && ( - - + +

No results found

Your search for "{search}" did not return any results

-
-
+ + )} {indexes.length > 0 && indexes.map((index) => ( - - + +

{index.schema}

-
- + +

{index.table}

-
- + +

{index.name}

-
- + +
-
-
+ + ))} - - } - /> +
+
+
)} diff --git a/apps/studio/components/interfaces/Database/Migrations/Migrations.tsx b/apps/studio/components/interfaces/Database/Migrations/Migrations.tsx index 0d7206b0fa7..ffe4a03f960 100644 --- a/apps/studio/components/interfaces/Database/Migrations/Migrations.tsx +++ b/apps/studio/components/interfaces/Database/Migrations/Migrations.tsx @@ -2,13 +2,23 @@ import dayjs from 'dayjs' import Link from 'next/link' import { useState } from 'react' -import Table from 'components/to-be-cleaned/Table' import CodeEditor from 'components/ui/CodeEditor/CodeEditor' import ShimmeringLoader from 'components/ui/ShimmeringLoader' import { DatabaseMigration, useMigrationsQuery } from 'data/database/migrations-query' import { useSelectedProjectQuery } from 'hooks/misc/useSelectedProject' import { Search } from 'lucide-react' -import { Button, Input, SidePanel } from 'ui' +import { + Button, + Card, + Input, + SidePanel, + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, +} from 'ui' import { Admonition } from 'ui-patterns' import MigrationsEmptyState from './MigrationsEmptyState' @@ -77,59 +87,63 @@ const Migrations = () => { icon={} /> - - Version - , - Name, - Inserted at (UTC), - , - ]} - body={ - migrations.length > 0 ? ( - migrations.map((migration) => { - // [Joshen] LEFT OFF HERE - const insertedAt = dayjs(migration.version, 'YYYYMMDDHHmmss').format( - 'DD MMM YYYY, HH:mm:ss' - ) + +
+ + + + Version + + Name + Inserted at (UTC) + + + + + {migrations.length > 0 ? ( + migrations.map((migration) => { + // [Joshen] LEFT OFF HERE + const insertedAt = dayjs(migration.version, 'YYYYMMDDHHmmss').format( + 'DD MMM YYYY, HH:mm:ss' + ) - return ( - - {migration.version} - - {migration?.name ?? 'Name not available'} - - {insertedAt} - - - - - ) - }) - ) : ( - - -

No results found

-

- Your search for "{search}" did not return any results -

-
-
- ) - } - /> + {migration?.name ?? 'Name not available'} + + {insertedAt} + + + + + ) + }) + ) : ( + + +

No results found

+

+ Your search for "{search}" did not return any results +

+
+
+ )} +
+
+ )} diff --git a/apps/studio/components/interfaces/Database/Publications/PublicationSkeleton.tsx b/apps/studio/components/interfaces/Database/Publications/PublicationSkeleton.tsx index c6774d0803c..70def772d88 100644 --- a/apps/studio/components/interfaces/Database/Publications/PublicationSkeleton.tsx +++ b/apps/studio/components/interfaces/Database/Publications/PublicationSkeleton.tsx @@ -1,32 +1,29 @@ -import Table from 'components/to-be-cleaned/Table' import ShimmeringLoader from 'components/ui/ShimmeringLoader' -import { Toggle } from 'ui' +import { Switch, TableCell, TableRow } from 'ui' export interface PublicationSkeletonProps { index?: number } -const PublicationSkeleton = ({ index }: PublicationSkeletonProps) => { +export const PublicationSkeleton = ({ index }: PublicationSkeletonProps) => { return ( - - + + - - + + - + {Array.from({ length: 4 }).map((_, i) => ( - - - + + + ))} - +
-
-
+ + ) } - -export default PublicationSkeleton diff --git a/apps/studio/components/interfaces/Database/Publications/PublicationsList.tsx b/apps/studio/components/interfaces/Database/Publications/PublicationsList.tsx index cd47055410a..deb947dd863 100644 --- a/apps/studio/components/interfaces/Database/Publications/PublicationsList.tsx +++ b/apps/studio/components/interfaces/Database/Publications/PublicationsList.tsx @@ -1,34 +1,52 @@ import { PermissionAction } from '@supabase/shared-types/out/constants' -import { noop } from 'lodash' +import { AlertCircle, Info, Search } from 'lucide-react' +import Link from 'next/link' import { useState } from 'react' import { toast } from 'sonner' -import { Button, Input, Toggle } from 'ui' -import ConfirmationModal from 'ui-patterns/Dialogs/ConfirmationModal' -import Table from 'components/to-be-cleaned/Table' +import { useParams } from 'common' +import AlertError from 'components/ui/AlertError' import InformationBox from 'components/ui/InformationBox' import NoSearchResults from 'components/ui/NoSearchResults' import { useDatabasePublicationsQuery } from 'data/database-publications/database-publications-query' import { useDatabasePublicationUpdateMutation } from 'data/database-publications/database-publications-update-mutation' -import { useCheckPermissions, usePermissionsLoaded } from 'hooks/misc/useCheckPermissions' +import { useAsyncCheckProjectPermissions } from 'hooks/misc/useCheckPermissions' import { useSelectedProjectQuery } from 'hooks/misc/useSelectedProject' -import { AlertCircle, Search } from 'lucide-react' -import PublicationSkeleton from './PublicationSkeleton' +import { + Button, + Card, + Switch, + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, + Tooltip, + TooltipContent, + TooltipTrigger, +} from 'ui' +import { Input } from 'ui-patterns/DataInputs/Input' +import ConfirmationModal from 'ui-patterns/Dialogs/ConfirmationModal' +import { PublicationSkeleton } from './PublicationSkeleton' interface PublicationEvent { event: string key: string } -interface PublicationsListProps { - onSelectPublication: (id: number) => void -} - -const PublicationsList = ({ onSelectPublication = noop }: PublicationsListProps) => { +export const PublicationsList = () => { + const { ref } = useParams() const { data: project } = useSelectedProjectQuery() const [filterString, setFilterString] = useState('') - const { data, isLoading } = useDatabasePublicationsQuery({ + const { + data = [], + error, + isLoading, + isSuccess, + isError, + } = useDatabasePublicationsQuery({ projectRef: project?.ref, connectionString: project?.connectionString, }) @@ -39,11 +57,8 @@ const PublicationsList = ({ onSelectPublication = noop }: PublicationsListProps) }, }) - const canUpdatePublications = useCheckPermissions( - PermissionAction.TENANT_SQL_ADMIN_WRITE, - 'publications' - ) - const isPermissionsLoaded = usePermissionsLoaded() + const { can: canUpdatePublications, isSuccess: isPermissionsLoaded } = + useAsyncCheckProjectPermissions(PermissionAction.TENANT_SQL_ADMIN_WRITE, 'publications') const publicationEvents: PublicationEvent[] = [ { event: 'Insert', key: 'publish_insert' }, @@ -51,10 +66,11 @@ const PublicationsList = ({ onSelectPublication = noop }: PublicationsListProps) { event: 'Delete', key: 'publish_delete' }, { event: 'Truncate', key: 'publish_truncate' }, ] - const publications = + const publications = ( filterString.length === 0 - ? data ?? [] - : (data ?? []).filter((publication) => publication.name.includes(filterString)) + ? data + : data.filter((publication) => publication.name.includes(filterString)) + ).sort((a, b) => a.id - b.id) const [toggleListenEventValue, setToggleListenEventValue] = useState<{ publication: any @@ -82,8 +98,9 @@ const PublicationsList = ({ onSelectPublication = noop }: PublicationsListProps)
} - placeholder={'Filter'} + icon={} + className="w-48 pl-8" + placeholder="Search for a publication" value={filterString} onChange={(e) => setFilterString(e.target.value)} /> @@ -100,32 +117,62 @@ const PublicationsList = ({ onSelectPublication = noop }: PublicationsListProps)
- Name, - System ID, - Insert, - Update, - Delete, - Truncate, - - Source - , - ]} - body={ - isLoading - ? Array.from({ length: 5 }).map((_, i) => ) - : publications.map((x) => ( - - {x.name} - {x.id} + +
+ + + Name + System ID + Insert + Update + Delete + Truncate + + + + + {isLoading && + Array.from({ length: 2 }).map((_, i) => )} + + {isError && ( + + + + + + )} + + {isSuccess && + publications.map((x) => ( + + + {x.name} + {/* [Joshen] Making this tooltip very specific for these 2 publications */} + {['supabase_realtime', 'supabase_realtime_messages_publication'].includes( + x.name + ) && ( + + + + + + {x.name === 'supabase_realtime' + ? 'This publication is managed by Supabase and handles Postgres changes' + : x.name === 'supabase_realtime_messages_publication' + ? 'This publication is managed by Supabase and handles broadcasts from the database' + : undefined} + + + )} + + {x.id} {publicationEvents.map((event) => ( - - + { + onClick={() => { setToggleListenEventValue({ publication: x, event, @@ -133,27 +180,24 @@ const PublicationsList = ({ onSelectPublication = noop }: PublicationsListProps) }) }} /> - + ))} - -
-
-
- - )) - } - /> + +
+ ))} +
+
+
{!isLoading && publications.length === 0 && ( @@ -183,5 +227,3 @@ const PublicationsList = ({ onSelectPublication = noop }: PublicationsListProps) ) } - -export default PublicationsList diff --git a/apps/studio/components/interfaces/Database/Publications/PublicationsTableItem.tsx b/apps/studio/components/interfaces/Database/Publications/PublicationsTableItem.tsx index 0628aefae27..b7a8425e002 100644 --- a/apps/studio/components/interfaces/Database/Publications/PublicationsTableItem.tsx +++ b/apps/studio/components/interfaces/Database/Publications/PublicationsTableItem.tsx @@ -1,28 +1,34 @@ import type { PostgresPublication, PostgresTable } from '@supabase/postgres-meta' import { PermissionAction } from '@supabase/shared-types/out/constants' import { useState } from 'react' -import { Badge, Toggle } from 'ui' - -import Table from 'components/to-be-cleaned/Table' -import { useDatabasePublicationUpdateMutation } from 'data/database-publications/database-publications-update-mutation' -import { useCheckPermissions } from 'hooks/misc/useCheckPermissions' -import { useSelectedProjectQuery } from 'hooks/misc/useSelectedProject' import { toast } from 'sonner' +import { useDatabasePublicationUpdateMutation } from 'data/database-publications/database-publications-update-mutation' +import { useAsyncCheckProjectPermissions } from 'hooks/misc/useCheckPermissions' +import { useSelectedProjectQuery } from 'hooks/misc/useSelectedProject' +import { useProtectedSchemas } from 'hooks/useProtectedSchemas' +import { Badge, Switch, TableCell, TableRow, Tooltip, TooltipContent, TooltipTrigger } from 'ui' + interface PublicationsTableItemProps { table: PostgresTable selectedPublication: PostgresPublication } -const PublicationsTableItem = ({ table, selectedPublication }: PublicationsTableItemProps) => { +export const PublicationsTableItem = ({ + table, + selectedPublication, +}: PublicationsTableItemProps) => { const { data: project } = useSelectedProjectQuery() + const { data: protectedSchemas } = useProtectedSchemas() const enabledForAllTables = selectedPublication.tables == null + const isProtected = protectedSchemas.map((x) => x.name).includes(table.schema) + const [checked, setChecked] = useState( selectedPublication.tables?.find((x: any) => x.id == table.id) != undefined ) - const canUpdatePublications = useCheckPermissions( + const { can: canUpdatePublications } = useAsyncCheckProjectPermissions( PermissionAction.TENANT_SQL_ADMIN_WRITE, 'publications' ) @@ -70,13 +76,13 @@ const PublicationsTableItem = ({ table, selectedPublication }: PublicationsTable } return ( - - {table.name} - {table.schema} - + + {table.name} + {table.schema} + {table.comment} - - + +
{enabledForAllTables ? ( @@ -84,19 +90,24 @@ const PublicationsTableItem = ({ table, selectedPublication }: PublicationsTable  for all tables ) : ( - toggleReplicationForTable(table, selectedPublication)} - /> + + + toggleReplicationForTable(table, selectedPublication)} + /> + + {isProtected && ( + + This table belongs to a protected schema, and its publication cannot be toggled + + )} + )}
-
-
+ + ) } - -export default PublicationsTableItem diff --git a/apps/studio/components/interfaces/Database/Publications/PublicationsTables.tsx b/apps/studio/components/interfaces/Database/Publications/PublicationsTables.tsx index 01b65876670..fe55650d79a 100644 --- a/apps/studio/components/interfaces/Database/Publications/PublicationsTables.tsx +++ b/apps/studio/components/interfaces/Database/Publications/PublicationsTables.tsx @@ -1,38 +1,38 @@ -import type { PostgresPublication } from '@supabase/postgres-meta' import { PermissionAction } from '@supabase/shared-types/out/constants' +import { ChevronLeft, Search } from 'lucide-react' +import Link from 'next/link' import { useMemo, useState } from 'react' +import { useParams } from 'common' import NoSearchResults from 'components/to-be-cleaned/NoSearchResults' -import Table from 'components/to-be-cleaned/Table' import AlertError from 'components/ui/AlertError' -import InformationBox from 'components/ui/InformationBox' +import { ButtonTooltip } from 'components/ui/ButtonTooltip' import { Loading } from 'components/ui/Loading' +import { useDatabasePublicationsQuery } from 'data/database-publications/database-publications-query' import { useTablesQuery } from 'data/tables/tables-query' -import { useCheckPermissions } from 'hooks/misc/useCheckPermissions' +import { useAsyncCheckProjectPermissions } from 'hooks/misc/useCheckPermissions' import { useSelectedProjectQuery } from 'hooks/misc/useSelectedProject' -import { useProtectedSchemas } from 'hooks/useProtectedSchemas' -import { AlertCircle, ChevronLeft, Search } from 'lucide-react' -import { Button, Input } from 'ui' -import PublicationsTableItem from './PublicationsTableItem' +import { Card, Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from 'ui' +import { Admonition } from 'ui-patterns' +import { Input } from 'ui-patterns/DataInputs/Input' +import { PublicationsTableItem } from './PublicationsTableItem' -interface PublicationsTablesProps { - selectedPublication: PostgresPublication - onSelectBack: () => void -} - -const PublicationsTables = ({ selectedPublication, onSelectBack }: PublicationsTablesProps) => { +export const PublicationsTables = () => { + const { ref, id } = useParams() const { data: project } = useSelectedProjectQuery() const [filterString, setFilterString] = useState('') - const canUpdatePublications = useCheckPermissions( - PermissionAction.TENANT_SQL_ADMIN_WRITE, - 'publications' - ) + const { can: canUpdatePublications, isLoading: isLoadingPermissions } = + useAsyncCheckProjectPermissions(PermissionAction.TENANT_SQL_ADMIN_WRITE, 'publications') - const { data: protectedSchemas } = useProtectedSchemas() + const { data: publications = [] } = useDatabasePublicationsQuery({ + projectRef: project?.ref, + connectionString: project?.connectionString, + }) + const selectedPublication = publications.find((pub) => pub.id === Number(id)) const { - data: tablesData, + data: tablesData = [], isLoading, isSuccess, isError, @@ -41,47 +41,49 @@ const PublicationsTables = ({ selectedPublication, onSelectBack }: PublicationsT projectRef: project?.ref, connectionString: project?.connectionString, }) + const tables = useMemo(() => { - return (tablesData || []).filter((table) => - filterString.length === 0 - ? !protectedSchemas.find((s) => s.name === table.schema) - : !protectedSchemas.find((s) => s.name === table.schema) && - table.name.includes(filterString) + return tablesData.filter((table) => + filterString.length === 0 ? table : table.name.includes(filterString) ) - }, [tablesData, protectedSchemas, filterString]) + }, [tablesData, filterString]) return ( <>
-
- {!canUpdatePublications && ( -
- } - title="You need additional permissions to update database replications" - /> -
+ {!isLoadingPermissions && !canUpdatePublications && ( + )}
- {isLoading && ( + + {(isLoading || isLoadingPermissions) && (
@@ -93,43 +95,43 @@ const PublicationsTables = ({ selectedPublication, onSelectBack }: PublicationsT (tables.length === 0 ? ( ) : ( -
- Name, - Schema, - - Description - , - - {/* Temporarily disable All tables toggle for publications. See https://github.com/supabase/supabase/pull/7233. -
-
- All Tables -
- toggleReplicationForAllTables(publication, enabledForAllTables)} - /> -
*/} -
, - ]} - body={tables.map((table) => ( - - ))} - /> - + +
+ + + Name + Schema + Description + {/* + We've disabled All tables toggle for publications. + See https://github.com/supabase/supabase/pull/7233. + */} + + + + + {!!selectedPublication ? ( + tables.map((table) => ( + + )) + ) : ( + + +

The selected publication with ID {id} cannot be found

+

+ Head back to the list of publications to select one from there +

+
+
+ )} +
+
+ ))} ) } - -export default PublicationsTables diff --git a/apps/studio/components/interfaces/Database/Replication/ReplicationPipelineStatus.tsx b/apps/studio/components/interfaces/Database/Replication/ReplicationPipelineStatus.tsx index 888bb679102..6205fdfb4b9 100644 --- a/apps/studio/components/interfaces/Database/Replication/ReplicationPipelineStatus.tsx +++ b/apps/studio/components/interfaces/Database/Replication/ReplicationPipelineStatus.tsx @@ -1,3 +1,7 @@ +// @ts-nocheck [Joshen] Temporarily silencing the TS checks here but please eventually remove +// it's cause the API types are conflicting a bit - API types have probably been updated for the UI here +// but the UI hasn't been updated yet to fit the new API types + import { Activity, ChevronLeft, ExternalLink, Search, X } from 'lucide-react' import Link from 'next/link' import { useEffect, useState } from 'react' diff --git a/apps/studio/components/interfaces/Database/Roles/CreateRolePanel.tsx b/apps/studio/components/interfaces/Database/Roles/CreateRolePanel.tsx index 0d604816196..7381c5968cf 100644 --- a/apps/studio/components/interfaces/Database/Roles/CreateRolePanel.tsx +++ b/apps/studio/components/interfaces/Database/Roles/CreateRolePanel.tsx @@ -44,7 +44,7 @@ const initialValues = { canBypassRls: false, } -const CreateRolePanel = ({ visible, onClose }: CreateRolePanelProps) => { +export const CreateRolePanel = ({ visible, onClose }: CreateRolePanelProps) => { const formId = 'create-new-role' const { data: project } = useSelectedProjectQuery() @@ -187,5 +187,3 @@ const CreateRolePanel = ({ visible, onClose }: CreateRolePanelProps) => { ) } - -export default CreateRolePanel diff --git a/apps/studio/components/interfaces/Database/Roles/DeleteRoleModal.tsx b/apps/studio/components/interfaces/Database/Roles/DeleteRoleModal.tsx index 9ae12e45753..91f868bc7b8 100644 --- a/apps/studio/components/interfaces/Database/Roles/DeleteRoleModal.tsx +++ b/apps/studio/components/interfaces/Database/Roles/DeleteRoleModal.tsx @@ -11,7 +11,7 @@ interface DeleteRoleModalProps { onClose: () => void } -const DeleteRoleModal = ({ role, visible, onClose }: DeleteRoleModalProps) => { +export const DeleteRoleModal = ({ role, visible, onClose }: DeleteRoleModalProps) => { const { data: project } = useSelectedProjectQuery() const { mutate: deleteDatabaseRole, isLoading: isDeleting } = useDatabaseRoleDeleteMutation({ @@ -50,5 +50,3 @@ const DeleteRoleModal = ({ role, visible, onClose }: DeleteRoleModalProps) => { ) } - -export default DeleteRoleModal diff --git a/apps/studio/components/interfaces/Database/Roles/RoleRow.tsx b/apps/studio/components/interfaces/Database/Roles/RoleRow.tsx index 5f109a3597a..876ffea6e01 100644 --- a/apps/studio/components/interfaces/Database/Roles/RoleRow.tsx +++ b/apps/studio/components/interfaces/Database/Roles/RoleRow.tsx @@ -27,7 +27,7 @@ interface RoleRowProps { onSelectDelete: (role: PgRole) => void } -const RoleRow = ({ role, disabled = false, onSelectDelete }: RoleRowProps) => { +export const RoleRow = ({ role, disabled = false, onSelectDelete }: RoleRowProps) => { const { data: project } = useSelectedProjectQuery() const [isExpanded, setIsExpanded] = useState(false) @@ -219,5 +219,3 @@ const RoleRow = ({ role, disabled = false, onSelectDelete }: RoleRowProps) => { ) } - -export default RoleRow diff --git a/apps/studio/components/interfaces/Database/Roles/RoleRowSkeleton.tsx b/apps/studio/components/interfaces/Database/Roles/RoleRowSkeleton.tsx index b0ef4317008..1b127aae5d6 100644 --- a/apps/studio/components/interfaces/Database/Roles/RoleRowSkeleton.tsx +++ b/apps/studio/components/interfaces/Database/Roles/RoleRowSkeleton.tsx @@ -6,7 +6,7 @@ export interface RoleRowSkeletonProps { index?: number } -const RoleRowSkeleton = ({ index }: RoleRowSkeletonProps) => { +export const RoleRowSkeleton = ({ index }: RoleRowSkeletonProps) => { return (
{
) } - -export default RoleRowSkeleton diff --git a/apps/studio/components/interfaces/Database/Roles/RolesList.tsx b/apps/studio/components/interfaces/Database/Roles/RolesList.tsx index 47568a0768e..1385c807f34 100644 --- a/apps/studio/components/interfaces/Database/Roles/RolesList.tsx +++ b/apps/studio/components/interfaces/Database/Roles/RolesList.tsx @@ -8,13 +8,13 @@ import NoSearchResults from 'components/ui/NoSearchResults' import SparkBar from 'components/ui/SparkBar' import { useDatabaseRolesQuery } from 'data/database-roles/database-roles-query' import { useMaxConnectionsQuery } from 'data/database/max-connections-query' -import { useCheckPermissions } from 'hooks/misc/useCheckPermissions' +import { useAsyncCheckProjectPermissions } from 'hooks/misc/useCheckPermissions' import { useSelectedProjectQuery } from 'hooks/misc/useSelectedProject' import { Badge, Button, Input, Tooltip, TooltipContent, TooltipTrigger } from 'ui' -import CreateRolePanel from './CreateRolePanel' -import DeleteRoleModal from './DeleteRoleModal' -import RoleRow from './RoleRow' -import RoleRowSkeleton from './RoleRowSkeleton' +import { CreateRolePanel } from './CreateRolePanel' +import { DeleteRoleModal } from './DeleteRoleModal' +import { RoleRow } from './RoleRow' +import { RoleRowSkeleton } from './RoleRowSkeleton' import { SUPABASE_ROLES } from './Roles.constants' type SUPABASE_ROLE = (typeof SUPABASE_ROLES)[number] @@ -27,7 +27,10 @@ const RolesList = () => { const [isCreatingRole, setIsCreatingRole] = useState(false) const [selectedRoleToDelete, setSelectedRoleToDelete] = useState() - const canUpdateRoles = useCheckPermissions(PermissionAction.TENANT_SQL_ADMIN_WRITE, 'roles') + const { can: canUpdateRoles } = useAsyncCheckProjectPermissions( + PermissionAction.TENANT_SQL_ADMIN_WRITE, + 'roles' + ) const { data: maxConnData } = useMaxConnectionsQuery({ projectRef: project?.ref, diff --git a/apps/studio/components/interfaces/Database/Tables/ColumnList.tsx b/apps/studio/components/interfaces/Database/Tables/ColumnList.tsx index ba4eedf9839..83aaf0cacef 100644 --- a/apps/studio/components/interfaces/Database/Tables/ColumnList.tsx +++ b/apps/studio/components/interfaces/Database/Tables/ColumnList.tsx @@ -4,6 +4,7 @@ import { Check, ChevronLeft, Edit, MoreVertical, Plus, Search, Trash, X } from ' import Link from 'next/link' import { useState } from 'react' +import { PostgresColumn } from '@supabase/postgres-meta' import { useParams } from 'common' import NoSearchResults from 'components/to-be-cleaned/NoSearchResults' import Table from 'components/to-be-cleaned/Table' @@ -12,7 +13,7 @@ import { ButtonTooltip } from 'components/ui/ButtonTooltip' import { GenericSkeletonLoader } from 'components/ui/ShimmeringLoader' import { useTableEditorQuery } from 'data/table-editor/table-editor-query' import { isTableLike } from 'data/table-editor/table-editor-types' -import { useCheckPermissions } from 'hooks/misc/useCheckPermissions' +import { useAsyncCheckProjectPermissions } from 'hooks/misc/useCheckPermissions' import { useSelectedProjectQuery } from 'hooks/misc/useSelectedProject' import { useIsProtectedSchema } from 'hooks/useProtectedSchemas' import { @@ -30,11 +31,11 @@ import { ProtectedSchemaWarning } from '../ProtectedSchemaWarning' interface ColumnListProps { onAddColumn: () => void - onEditColumn: (column: any) => void - onDeleteColumn: (column: any) => void + onEditColumn: (column: PostgresColumn) => void + onDeleteColumn: (column: PostgresColumn) => void } -const ColumnList = ({ +export const ColumnList = ({ onAddColumn = noop, onEditColumn = noop, onDeleteColumn = noop, @@ -64,7 +65,10 @@ const ColumnList = ({ : selectedTable?.columns?.filter((column) => column.name.includes(filterString))) ?? [] const { isSchemaLocked } = useIsProtectedSchema({ schema: selectedTable?.schema ?? '' }) - const canUpdateColumns = useCheckPermissions(PermissionAction.TENANT_SQL_ADMIN_WRITE, 'columns') + const { can: canUpdateColumns } = useAsyncCheckProjectPermissions( + PermissionAction.TENANT_SQL_ADMIN_WRITE, + 'columns' + ) return (
@@ -213,5 +217,3 @@ const ColumnList = ({
) } - -export default ColumnList diff --git a/apps/studio/components/interfaces/Database/Tables/TableList.tsx b/apps/studio/components/interfaces/Database/Tables/TableList.tsx index c3eea9f3055..5d08596a521 100644 --- a/apps/studio/components/interfaces/Database/Tables/TableList.tsx +++ b/apps/studio/components/interfaces/Database/Tables/TableList.tsx @@ -20,9 +20,9 @@ import { useRouter } from 'next/router' import { useState } from 'react' import { useParams } from 'common' -import Table from 'components/to-be-cleaned/Table' import AlertError from 'components/ui/AlertError' import { ButtonTooltip } from 'components/ui/ButtonTooltip' +import { DropdownMenuItemTooltip } from 'components/ui/DropdownMenuItemTooltip' import SchemaSelector from 'components/ui/SchemaSelector' import { GenericSkeletonLoader } from 'components/ui/ShimmeringLoader' import { useDatabasePublicationsQuery } from 'data/database-publications/database-publications-query' @@ -32,12 +32,13 @@ import { useMaterializedViewsQuery } from 'data/materialized-views/materialized- import { usePrefetchEditorTablePage } from 'data/prefetchers/project.$ref.editor.$id' import { useTablesQuery } from 'data/tables/tables-query' import { useViewsQuery } from 'data/views/views-query' -import { useCheckPermissions } from 'hooks/misc/useCheckPermissions' +import { useAsyncCheckProjectPermissions } from 'hooks/misc/useCheckPermissions' import { useQuerySchemaState } from 'hooks/misc/useSchemaQueryState' import { useSelectedProjectQuery } from 'hooks/misc/useSelectedProject' import { useIsProtectedSchema } from 'hooks/useProtectedSchemas' import { Button, + Card, Checkbox_Shadcn_, DropdownMenu, DropdownMenuContent, @@ -52,6 +53,12 @@ import { Tooltip, TooltipContent, TooltipTrigger, + Table, + TableHeader, + TableHead, + TableBody, + TableRow, + TableCell, cn, } from 'ui' import { ProtectedSchemaWarning } from '../ProtectedSchemaWarning' @@ -64,7 +71,7 @@ interface TableListProps { onDuplicateTable: (table: PostgresTable) => void } -const TableList = ({ +export const TableList = ({ onDuplicateTable, onAddTable = noop, onEditTable = noop, @@ -80,7 +87,11 @@ const TableList = ({ const [filterString, setFilterString] = useState('') const [visibleTypes, setVisibleTypes] = useState(Object.values(ENTITY_TYPE)) - const canUpdateTables = useCheckPermissions(PermissionAction.TENANT_SQL_ADMIN_WRITE, 'tables') + + const { can: canUpdateTables } = useAsyncCheckProjectPermissions( + PermissionAction.TENANT_SQL_ADMIN_WRITE, + 'tables' + ) const { data: tables, @@ -298,264 +309,267 @@ const TableList = ({ {isSuccess && (
- , - Name, - - Description - , - - Rows (Estimated) - , - - Size (Estimated) - , - - Realtime Enabled - , - , - ]} - body={ - <> - {entities.length === 0 && filterString.length === 0 && ( - - - {visibleTypes.length === 0 ? ( - <> -

- Please select at least one entity type to filter with -

-

- There are currently no results based on the filter that you have applied -

- - ) : ( - <> -

No tables created yet

-

- There are no{' '} - {visibleTypes.length === 5 - ? 'tables' - : visibleTypes.length === 1 - ? `${formatTooltipText(visibleTypes[0])}s` - : `${visibleTypes - .slice(0, -1) - .map((x) => `${formatTooltipText(x)}s`) - .join( - ', ' - )}, and ${formatTooltipText(visibleTypes[visibleTypes.length - 1])}s`}{' '} - found in the schema "{selectedSchema}" -

- - )} -
-
- )} - {entities.length === 0 && filterString.length > 0 && ( - - -

No results found

-

- Your search for "{filterString}" did not return any results -

-
-
- )} - {entities.length > 0 && - entities.map((x) => ( - - - - - {x.type === ENTITY_TYPE.TABLE ? ( - - ) : x.type === ENTITY_TYPE.VIEW ? ( - - ) : ( -
- {Object.entries(ENTITY_TYPE) - .find(([, value]) => value === x.type)?.[0]?.[0] - ?.toUpperCase()} -
- )} -
- - {formatTooltipText(x.type)} - -
-
- - {/* only show tooltips if required, to reduce noise */} - {x.name.length > 20 ? ( - - -

{x.name}

-
- - {x.name} -
+ +
+ + + + Name + + Description + + + Rows (Estimated) + + + Size (Estimated) + + + Realtime Enabled + + + + + + <> + {entities.length === 0 && filterString.length === 0 && ( + + + {visibleTypes.length === 0 ? ( + <> +

+ Please select at least one entity type to filter with +

+

+ There are currently no results based on the filter that you have + applied +

+ ) : ( -

{x.name}

+ <> +

No tables created yet

+

+ There are no{' '} + {visibleTypes.length === 5 + ? 'tables' + : visibleTypes.length === 1 + ? `${formatTooltipText(visibleTypes[0])}s` + : `${visibleTypes + .slice(0, -1) + .map((x) => `${formatTooltipText(x)}s`) + .join( + ', ' + )}, and ${formatTooltipText(visibleTypes[visibleTypes.length - 1])}s`}{' '} + found in the schema "{selectedSchema}" +

+ )} - - - {x.comment !== null ? ( - - {x.comment} - - ) : ( -

No description

- )} -
- - {x.rows !== undefined ? x.rows.toLocaleString() : '-'} - - - {x.size !== undefined ? {x.size} : '-'} - - - {(realtimePublication?.tables ?? []).find((table) => table.id === x.id) ? ( -
- -
- ) : ( -
- -
- )} -
- -
- - - {!isSchemaLocked && ( - - -
-
- - ))} - - } - /> +
+ + {x.comment !== null ? ( + + {x.comment} + + ) : ( +

No description

+ )} +
+ + {x.rows !== undefined ? x.rows.toLocaleString() : '-'} + + + {x.size !== undefined ? {x.size} : '-'} + + + {(realtimePublication?.tables ?? []).find( + (table) => table.id === x.id + ) ? ( +
+ +
+ ) : ( +
+ +
+ )} +
+ +
+ + + {!isSchemaLocked && ( + + +
+
+
+ ))} + +
+
+
)}
) } - -export default TableList diff --git a/apps/studio/components/interfaces/Database/Triggers/TriggersList/TriggerList.tsx b/apps/studio/components/interfaces/Database/Triggers/TriggersList/TriggerList.tsx index 809b5d65bcc..f78e526252b 100644 --- a/apps/studio/components/interfaces/Database/Triggers/TriggersList/TriggerList.tsx +++ b/apps/studio/components/interfaces/Database/Triggers/TriggersList/TriggerList.tsx @@ -5,7 +5,7 @@ import { Check, Edit, Edit2, MoreVertical, Trash, X } from 'lucide-react' import Table from 'components/to-be-cleaned/Table' import { ButtonTooltip } from 'components/ui/ButtonTooltip' import { useDatabaseTriggersQuery } from 'data/database-triggers/database-triggers-query' -import { useCheckPermissions } from 'hooks/misc/useCheckPermissions' +import { useAsyncCheckProjectPermissions } from 'hooks/misc/useCheckPermissions' import { useSelectedProjectQuery } from 'hooks/misc/useSelectedProject' import { useAiAssistantStateSnapshot } from 'state/ai-assistant-state' import { @@ -18,6 +18,8 @@ import { Tooltip, TooltipContent, TooltipTrigger, + TableRow, + TableCell, } from 'ui' import { generateTriggerCreateSQL } from './TriggerList.utils' @@ -51,39 +53,42 @@ const TriggerList = ({ filteredTriggers.filter((x) => x.schema == schema), (trigger) => trigger.name.toLocaleLowerCase() ) - const canUpdateTriggers = useCheckPermissions(PermissionAction.TENANT_SQL_ADMIN_WRITE, 'triggers') + const { can: canUpdateTriggers } = useAsyncCheckProjectPermissions( + PermissionAction.TENANT_SQL_ADMIN_WRITE, + 'triggers' + ) if (_triggers.length === 0 && filterString.length === 0) { return ( - - + +

No triggers created yet

There are no triggers found in the schema "{schema}"

-
-
+ + ) } if (_triggers.length === 0 && filterString.length > 0) { return ( - - + +

No results found

Your search for "{filterString}" did not return any results

-
-
+ + ) } return ( <> {_triggers.map((x: any) => ( - - + + editTrigger(x)} @@ -95,35 +100,35 @@ const TriggerList = ({ {x.name} - + - +

{x.table}

-
+ - +

{x.function_name}

-
+ - +
{x.events.map((event: string) => ( {`${x.activation} ${event}`} ))}
-
+ - +

{x.orientation}

-
+ - +
{x.enabled_mode !== 'DISABLED' ? ( @@ -131,9 +136,9 @@ const TriggerList = ({ )}
-
+ - + {!isLocked && (
{canUpdateTriggers ? ( @@ -213,8 +218,8 @@ const TriggerList = ({ )}
)} -
-
+ + ))} ) diff --git a/apps/studio/components/interfaces/Database/Triggers/TriggersList/TriggersList.tsx b/apps/studio/components/interfaces/Database/Triggers/TriggersList/TriggersList.tsx index 6cb57562621..a9097f7190d 100644 --- a/apps/studio/components/interfaces/Database/Triggers/TriggersList/TriggersList.tsx +++ b/apps/studio/components/interfaces/Database/Triggers/TriggersList/TriggersList.tsx @@ -6,19 +6,28 @@ import { useState } from 'react' import AlphaPreview from 'components/to-be-cleaned/AlphaPreview' import ProductEmptyState from 'components/to-be-cleaned/ProductEmptyState' -import Table from 'components/to-be-cleaned/Table' import AlertError from 'components/ui/AlertError' import { ButtonTooltip } from 'components/ui/ButtonTooltip' import SchemaSelector from 'components/ui/SchemaSelector' import { GenericSkeletonLoader } from 'components/ui/ShimmeringLoader' import { useDatabaseTriggersQuery } from 'data/database-triggers/database-triggers-query' import { useTablesQuery } from 'data/tables/tables-query' -import { useCheckPermissions } from 'hooks/misc/useCheckPermissions' +import { useAsyncCheckProjectPermissions } from 'hooks/misc/useCheckPermissions' import { useQuerySchemaState } from 'hooks/misc/useSchemaQueryState' import { useSelectedProjectQuery } from 'hooks/misc/useSelectedProject' import { useIsProtectedSchema, useProtectedSchemas } from 'hooks/useProtectedSchemas' import { useAiAssistantStateSnapshot } from 'state/ai-assistant-state' -import { AiIconAnimation, Input } from 'ui' +import { + AiIconAnimation, + Input, + Card, + Table, + TableHeader, + TableHead, + TableBody, + TableRow, + TableCell, +} from 'ui' import { ProtectedSchemaWarning } from '../../ProtectedSchemaWarning' import TriggerList from './TriggerList' @@ -41,7 +50,7 @@ const TriggersList = ({ const { data: protectedSchemas } = useProtectedSchemas() const { isSchemaLocked } = useIsProtectedSchema({ schema: selectedSchema }) - const { data = [], isSuccess } = useTablesQuery({ + const { data = [] } = useTablesQuery({ projectRef: project?.ref, connectionString: project?.connectionString, }) @@ -58,7 +67,10 @@ const TriggersList = ({ connectionString: project?.connectionString, }) - const canCreateTriggers = useCheckPermissions(PermissionAction.TENANT_SQL_ADMIN_WRITE, 'triggers') + const { can: canCreateTriggers } = useAsyncCheckProjectPermissions( + PermissionAction.TENANT_SQL_ADMIN_WRITE, + 'triggers' + ) if (isLoading) { return @@ -178,30 +190,32 @@ const TriggersList = ({ {isSchemaLocked && }
- - Name - Table - Function - Events - Orientation - - Enabled - - - - } - body={ - - } - /> + +
+ + + Name + Table + Function + Events + Orientation + + Enabled + + + + + + + +
+
)} diff --git a/apps/studio/components/interfaces/Database/index.ts b/apps/studio/components/interfaces/Database/index.ts index 0667d17b0dc..0269df17345 100644 --- a/apps/studio/components/interfaces/Database/index.ts +++ b/apps/studio/components/interfaces/Database/index.ts @@ -1,13 +1,7 @@ -export { default as ColumnList } from './Tables/ColumnList' -export { default as TableList } from './Tables/TableList' - export { default as RolesList } from './Roles/RolesList' export { default as Extensions } from './Extensions/Extensions' -export { default as PublicationsList } from './Publications/PublicationsList' -export { default as PublicationsTables } from './Publications/PublicationsTables' - export { default as BackupsList } from './Backups/BackupsList' export { default as CreateFunction } from './Functions/CreateFunction' diff --git a/apps/studio/components/interfaces/DiskManagement/DiskManagementForm.tsx b/apps/studio/components/interfaces/DiskManagement/DiskManagementForm.tsx index 85e194c4f37..8cb6d6c6ee0 100644 --- a/apps/studio/components/interfaces/DiskManagement/DiskManagementForm.tsx +++ b/apps/studio/components/interfaces/DiskManagement/DiskManagementForm.tsx @@ -25,7 +25,7 @@ import { useProjectAddonUpdateMutation } from 'data/subscriptions/project-addon- import { useProjectAddonsQuery } from 'data/subscriptions/project-addons-query' import { AddonVariantId } from 'data/subscriptions/types' import { useResourceWarningsQuery } from 'data/usage/resource-warnings-query' -import { useCheckPermissions, usePermissionsLoaded } from 'hooks/misc/useCheckPermissions' +import { useAsyncCheckProjectPermissions } from 'hooks/misc/useCheckPermissions' import { useSelectedOrganizationQuery } from 'hooks/misc/useSelectedOrganization' import { useIsAwsCloudProvider, @@ -66,8 +66,7 @@ import { NoticeBar } from './ui/NoticeBar' import { SpendCapDisabledSection } from './ui/SpendCapDisabledSection' export function DiskManagementForm() { - // isLoading is used to avoid a useCheckPermissions() race condition - const { data: project, isLoading: isProjectLoading } = useSelectedProjectQuery() + const { data: project } = useSelectedProjectQuery() const { data: org } = useSelectedOrganizationQuery() const { ref: projectRef } = useParams() const queryClient = useQueryClient() @@ -80,27 +79,18 @@ export function DiskManagementForm() { const isAws = useIsAwsCloudProvider() const isAwsK8s = useIsAwsK8sCloudProvider() - /** - * Permissions - */ - const isPermissionsLoaded = usePermissionsLoaded() - const canUpdateDiskConfiguration = useCheckPermissions(PermissionAction.UPDATE, 'projects', { - resource: { - project_id: project?.id, - }, - }) + const { can: canUpdateDiskConfiguration, isSuccess: isPermissionsLoaded } = + useAsyncCheckProjectPermissions(PermissionAction.UPDATE, 'projects', { + resource: { + project_id: project?.id, + }, + }) - /** - * Component States - */ const [isDialogOpen, setIsDialogOpen] = useState(false) const [refetchInterval, setRefetchInterval] = useState(false) const [message, setMessageState] = useState(null) const [advancedSettingsOpen, setAdvancedSettingsOpenState] = useState(false) - /** - * Fetch form data - */ const { data: databases, isSuccess: isReadReplicasSuccess } = useReadReplicasQuery({ projectRef }) const { data, isSuccess: isDiskAttributesSuccess } = useDiskAttributesQuery( { projectRef }, @@ -146,9 +136,6 @@ export function DiskManagementForm() { const { data: diskAutoscaleConfig, isSuccess: isDiskAutoscaleConfigSuccess } = useDiskAutoscaleCustomConfigQuery({ projectRef }, { enabled: project != null && isAws }) - /** - * Handle default values - */ const computeSize = project?.infra_compute_size ? mapComputeSizeNameToAddonVariantId(project?.infra_compute_size) : undefined @@ -190,10 +177,6 @@ export function DiskManagementForm() { } }, [modifiedComputeSize, isDialogOpen, project]) - /** - * State handling - */ - const isSuccess = isAddonsSuccess && isDiskAttributesSuccess && @@ -228,7 +211,7 @@ export function DiskManagementForm() { const isDirty = !!Object.keys(form.formState.dirtyFields).length const isProjectResizing = project?.status === PROJECT_STATUS.RESIZING const isProjectRequestingDiskChanges = isRequestingChanges && !isProjectResizing - const noPermissions = isPermissionsLoaded && !canUpdateDiskConfiguration && !isProjectLoading + const noPermissions = isPermissionsLoaded && !canUpdateDiskConfiguration const { mutateAsync: updateDiskConfiguration, isLoading: isUpdatingDisk } = useUpdateDiskAttributesMutation({ diff --git a/apps/studio/components/interfaces/DiskManagement/DiskManagementReviewAndSubmitDialog.tsx b/apps/studio/components/interfaces/DiskManagement/DiskManagementReviewAndSubmitDialog.tsx index 03b3d42a7f2..89512705ff5 100644 --- a/apps/studio/components/interfaces/DiskManagement/DiskManagementReviewAndSubmitDialog.tsx +++ b/apps/studio/components/interfaces/DiskManagement/DiskManagementReviewAndSubmitDialog.tsx @@ -5,7 +5,7 @@ import { UseFormReturn } from 'react-hook-form' import { ButtonTooltip } from 'components/ui/ButtonTooltip' import { useProjectAddonsQuery } from 'data/subscriptions/project-addons-query' -import { useCheckPermissions } from 'hooks/misc/useCheckPermissions' +import { useAsyncCheckProjectPermissions } from 'hooks/misc/useCheckPermissions' import { useSelectedOrganizationQuery } from 'hooks/misc/useSelectedOrganization' import { useSelectedProjectQuery } from 'hooks/misc/useSelectedProject' import { formatCurrency } from 'lib/helpers' @@ -153,11 +153,15 @@ export const DiskManagementReviewAndSubmitDialog = ({ const { formState, getValues } = form - const canUpdateDiskConfiguration = useCheckPermissions(PermissionAction.UPDATE, 'projects', { - resource: { - project_id: project?.id, - }, - }) + const { can: canUpdateDiskConfiguration } = useAsyncCheckProjectPermissions( + PermissionAction.UPDATE, + 'projects', + { + resource: { + project_id: project?.id, + }, + } + ) /** * Queries diff --git a/apps/studio/components/interfaces/Docs/Description.tsx b/apps/studio/components/interfaces/Docs/Description.tsx index dc9dfb112d7..b2e6176400f 100644 --- a/apps/studio/components/interfaces/Docs/Description.tsx +++ b/apps/studio/components/interfaces/Docs/Description.tsx @@ -5,7 +5,7 @@ import { toast } from 'sonner' import AutoTextArea from 'components/to-be-cleaned/forms/AutoTextArea' import { executeSql } from 'data/sql/execute-sql-query' -import { useCheckPermissions } from 'hooks/misc/useCheckPermissions' +import { useAsyncCheckProjectPermissions } from 'hooks/misc/useCheckPermissions' import { useSelectedProjectQuery } from 'hooks/misc/useSelectedProject' import { timeout } from 'lib/helpers' import { Loader } from 'lucide-react' @@ -42,7 +42,10 @@ const Description = ({ content, metadata, onChange = noop }: DescrptionProps) => const hasChanged = value != contentText const animateCss = `transition duration-150` - const canUpdateDescription = useCheckPermissions(PermissionAction.TENANT_SQL_QUERY, '*') + const { can: canUpdateDescription } = useAsyncCheckProjectPermissions( + PermissionAction.TENANT_SQL_QUERY, + '*' + ) const updateDescription = async () => { if (isUpdating || !canUpdateDescription) return false @@ -76,7 +79,9 @@ const Description = ({ content, metadata, onChange = noop }: DescrptionProps) => if (!canUpdateDescription) { return ( - {value || 'No description'} + + {value || 'No description'} + ) } diff --git a/apps/studio/components/interfaces/Functions/EdgeFunctionDetails/EdgeFunctionDetails.tsx b/apps/studio/components/interfaces/Functions/EdgeFunctionDetails/EdgeFunctionDetails.tsx index 6358d77eac5..efde692501c 100644 --- a/apps/studio/components/interfaces/Functions/EdgeFunctionDetails/EdgeFunctionDetails.tsx +++ b/apps/studio/components/interfaces/Functions/EdgeFunctionDetails/EdgeFunctionDetails.tsx @@ -18,7 +18,7 @@ import { useCustomDomainsQuery } from 'data/custom-domains/custom-domains-query' import { useEdgeFunctionQuery } from 'data/edge-functions/edge-function-query' import { useEdgeFunctionDeleteMutation } from 'data/edge-functions/edge-functions-delete-mutation' import { useEdgeFunctionUpdateMutation } from 'data/edge-functions/edge-functions-update-mutation' -import { useCheckPermissions } from 'hooks/misc/useCheckPermissions' +import { useAsyncCheckProjectPermissions } from 'hooks/misc/useCheckPermissions' import { Alert_Shadcn_, AlertDescription_Shadcn_, @@ -59,7 +59,10 @@ export const EdgeFunctionDetails = () => { const router = useRouter() const { ref: projectRef, functionSlug } = useParams() const [showDeleteModal, setShowDeleteModal] = useState(false) - const canUpdateEdgeFunction = useCheckPermissions(PermissionAction.FUNCTIONS_WRITE, '*') + const { can: canUpdateEdgeFunction } = useAsyncCheckProjectPermissions( + PermissionAction.FUNCTIONS_WRITE, + '*' + ) const { data: apiKeys } = useAPIKeysQuery({ projectRef }) const { data: settings } = useProjectSettingsV2Query({ projectRef }) diff --git a/apps/studio/components/interfaces/Functions/EdgeFunctionSecrets/AddNewSecretForm.tsx b/apps/studio/components/interfaces/Functions/EdgeFunctionSecrets/AddNewSecretForm.tsx index ae512e6e275..9c1826ef9b6 100644 --- a/apps/studio/components/interfaces/Functions/EdgeFunctionSecrets/AddNewSecretForm.tsx +++ b/apps/studio/components/interfaces/Functions/EdgeFunctionSecrets/AddNewSecretForm.tsx @@ -12,6 +12,11 @@ import { Eye, EyeOff, MinusCircle } from 'lucide-react' import { DuplicateSecretWarningModal } from './DuplicateSecretWarningModal' import { Button, + Card, + CardContent, + CardFooter, + CardHeader, + CardTitle, Form_Shadcn_, FormControl_Shadcn_, FormField_Shadcn_, @@ -149,91 +154,93 @@ const AddNewSecretForm = () => { } return ( - - -

Add new secrets

- -
- {fields.map((fieldItem, index) => ( -
- ( - - Key - - handlePaste(e.nativeEvent)} - /> - - - - )} - /> - ( - - Value - - -
- } - /> - - - - )} - /> + <> + + + + + Add new secrets + + + {fields.map((fieldItem, index) => ( +
+ ( + + Key + + handlePaste(e.nativeEvent)} + /> + + + + )} + /> + ( + + Value + + +
+ } + /> + + + + )} + /> - - -
+ + + -
- -
-
- + + + + { isCreating={isCreating} secretName={duplicateSecretName} /> -
+ ) } diff --git a/apps/studio/components/interfaces/Functions/EdgeFunctionSecrets/EdgeFunctionSecret.tsx b/apps/studio/components/interfaces/Functions/EdgeFunctionSecrets/EdgeFunctionSecret.tsx index 4821b1a3093..16edc741a3f 100644 --- a/apps/studio/components/interfaces/Functions/EdgeFunctionSecrets/EdgeFunctionSecret.tsx +++ b/apps/studio/components/interfaces/Functions/EdgeFunctionSecrets/EdgeFunctionSecret.tsx @@ -1,10 +1,10 @@ import { PermissionAction } from '@supabase/shared-types/out/constants' import { Trash } from 'lucide-react' -import Table from 'components/to-be-cleaned/Table' import { ButtonTooltip } from 'components/ui/ButtonTooltip' import type { ProjectSecret } from 'data/secrets/secrets-query' -import { useCheckPermissions } from 'hooks/misc/useCheckPermissions' +import { useAsyncCheckProjectPermissions } from 'hooks/misc/useCheckPermissions' +import { TableCell, TableRow } from 'ui' import { TimestampInfo } from 'ui-patterns' interface EdgeFunctionSecretProps { @@ -13,34 +13,40 @@ interface EdgeFunctionSecretProps { } const EdgeFunctionSecret = ({ secret, onSelectDelete }: EdgeFunctionSecretProps) => { - const canUpdateSecrets = useCheckPermissions(PermissionAction.SECRETS_WRITE, '*') + const { can: canUpdateSecrets } = useAsyncCheckProjectPermissions( + PermissionAction.SECRETS_WRITE, + '*' + ) // [Joshen] Following API's validation: // https://github.com/supabase/infrastructure/blob/develop/api/src/routes/v1/projects/ref/secrets/secrets.controller.ts#L106 const isReservedSecret = !!secret.name.match(/^(SUPABASE_).*/) return ( - - + +

{secret.name}

-
- -

+ + +

{secret.value}

-
- + + {!!secret.updated_at ? ( ) : ( '-' )} - - + +
-
-
+ + ) } diff --git a/apps/studio/components/interfaces/Functions/EdgeFunctionSecrets/EdgeFunctionSecrets.tsx b/apps/studio/components/interfaces/Functions/EdgeFunctionSecrets/EdgeFunctionSecrets.tsx index 4aba64bac4e..00d859249f9 100644 --- a/apps/studio/components/interfaces/Functions/EdgeFunctionSecrets/EdgeFunctionSecrets.tsx +++ b/apps/studio/components/interfaces/Functions/EdgeFunctionSecrets/EdgeFunctionSecrets.tsx @@ -4,14 +4,13 @@ import { useState } from 'react' import { toast } from 'sonner' import { useParams } from 'common' -import Table from 'components/to-be-cleaned/Table' import AlertError from 'components/ui/AlertError' import NoPermission from 'components/ui/NoPermission' import { GenericSkeletonLoader } from 'components/ui/ShimmeringLoader' import { useSecretsDeleteMutation } from 'data/secrets/secrets-delete-mutation' import { ProjectSecret, useSecretsQuery } from 'data/secrets/secrets-query' -import { useCheckPermissions } from 'hooks/misc/useCheckPermissions' -import { Badge, Separator } from 'ui' +import { useAsyncCheckProjectPermissions } from 'hooks/misc/useCheckPermissions' +import { Badge, Card, Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from 'ui' import { Input } from 'ui-patterns/DataInputs/Input' import ConfirmationModal from 'ui-patterns/Dialogs/ConfirmationModal' import AddNewSecretForm from './AddNewSecretForm' @@ -22,8 +21,14 @@ const EdgeFunctionSecrets = () => { const [searchString, setSearchString] = useState('') const [selectedSecret, setSelectedSecret] = useState() - const canReadSecrets = useCheckPermissions(PermissionAction.SECRETS_READ, '*') - const canUpdateSecrets = useCheckPermissions(PermissionAction.SECRETS_WRITE, '*') + const { can: canReadSecrets, isLoading: isLoadingPermissions } = useAsyncCheckProjectPermissions( + PermissionAction.SECRETS_READ, + '*' + ) + const { can: canUpdateSecrets } = useAsyncCheckProjectPermissions( + PermissionAction.SECRETS_WRITE, + '*' + ) const { data, error, isLoading, isSuccess, isError } = useSecretsQuery({ projectRef: projectRef, @@ -43,82 +48,89 @@ const EdgeFunctionSecrets = () => { : data ?? [] const headers = [ - Name, - + Name, + Digest{' '} SHA256 - , - Updated at, - , + , + Updated at, + , ] return ( <> - {isLoading && } - {isError && } - {isSuccess && ( + {isLoading || isLoadingPermissions ? ( + + ) : ( <> - {!canUpdateSecrets ? ( - - ) : ( -
- - -
- )} - {canUpdateSecrets && !canReadSecrets ? ( - - ) : canReadSecrets ? ( -
-
- setSearchString(e.target.value)} - icon={} - /> -
+ {isError && } -
- 0 ? ( - secrets.map((secret) => ( - setSelectedSecret(secret)} - /> - )) - ) : secrets.length === 0 && searchString.length > 0 ? ( - - -

No results found

-

- Your search for "{searchString}" did not return any results -

-
-
- ) : ( - - -

No secrets created

-

- There are no secrets associated with your project yet -

-
-
- ) - } - /> + {isSuccess && ( + <> +
+ {!canUpdateSecrets ? ( + + ) : ( + + )}
- - ) : null} + {canUpdateSecrets && !canReadSecrets ? ( + + ) : canReadSecrets ? ( +
+
+ setSearchString(e.target.value)} + icon={} + /> +
+ + +
+ + {headers} + + + {secrets.length > 0 ? ( + secrets.map((secret) => ( + setSelectedSecret(secret)} + /> + )) + ) : secrets.length === 0 && searchString.length > 0 ? ( + + +

No results found

+

+ Your search for "{searchString}" did not return any results +

+
+
+ ) : ( + + +

No secrets created

+

+ There are no secrets associated with your project yet +

+
+
+ )} +
+
+ +
+ ) : null} + + )} )} diff --git a/apps/studio/components/interfaces/Functions/EdgeFunctionsListItem.tsx b/apps/studio/components/interfaces/Functions/EdgeFunctionsListItem.tsx index 55fae1e47a5..186616f953e 100644 --- a/apps/studio/components/interfaces/Functions/EdgeFunctionsListItem.tsx +++ b/apps/studio/components/interfaces/Functions/EdgeFunctionsListItem.tsx @@ -8,7 +8,7 @@ import Table from 'components/to-be-cleaned/Table' import { useCustomDomainsQuery } from 'data/custom-domains/custom-domains-query' import type { EdgeFunctionsResponse } from 'data/edge-functions/edge-functions-query' import { useSelectedProjectQuery } from 'hooks/misc/useSelectedProject' -import { copyToClipboard, Tooltip, TooltipContent, TooltipTrigger } from 'ui' +import { copyToClipboard, TableCell, TableRow, Tooltip, TooltipContent, TooltipTrigger } from 'ui' interface EdgeFunctionsListItemProps { function: EdgeFunctionsResponse @@ -33,18 +33,19 @@ export const EdgeFunctionsListItem = ({ function: item }: EdgeFunctionsListItemP : functionUrl return ( - { router.push(`/project/${ref}/functions/${item.slug}`) }} + className="cursor-pointer" > - +

{item.name}

-
- + +

{endpoint} @@ -75,13 +76,13 @@ export const EdgeFunctionsListItem = ({ function: item }: EdgeFunctionsListItemP )}

-
- + +

{dayjs(item.created_at).format('DD MMM, YYYY HH:mm')}

-
- + +
@@ -92,10 +93,10 @@ export const EdgeFunctionsListItem = ({ function: item }: EdgeFunctionsListItemP Last updated on {dayjs(item.updated_at).format('DD MMM, YYYY HH:mm')} - - + +

{item.version}

-
- + + ) } diff --git a/apps/studio/components/interfaces/GraphQL/GraphiQL.tsx b/apps/studio/components/interfaces/GraphQL/GraphiQL.tsx index 82f5714e963..c7351a4db71 100644 --- a/apps/studio/components/interfaces/GraphQL/GraphiQL.tsx +++ b/apps/studio/components/interfaces/GraphQL/GraphiQL.tsx @@ -35,12 +35,12 @@ import { PermissionAction } from '@supabase/shared-types/out/constants' import { AlertTriangle, XIcon } from 'lucide-react' import { MouseEventHandler, useCallback, useEffect, useState } from 'react' -import { useCheckPermissions } from 'hooks/misc/useCheckPermissions' +import { LOCAL_STORAGE_KEYS } from 'common' +import { useAsyncCheckProjectPermissions } from 'hooks/misc/useCheckPermissions' import { useLocalStorage } from 'hooks/misc/useLocalStorage' import { AlertDescription_Shadcn_, AlertTitle_Shadcn_, Alert_Shadcn_, Button, cn } from 'ui' import { RoleImpersonationSelector } from '../RoleImpersonationSelector' import styles from './graphiql.module.css' -import { LOCAL_STORAGE_KEYS } from 'common' export interface GraphiQLProps { fetcher: Fetcher @@ -76,7 +76,10 @@ const GraphiQLInterface = ({ theme }: GraphiQLInterfaceProps) => { const merge = useMergeQuery() const prettify = usePrettifyEditors() - const canReadJWTSecret = useCheckPermissions(PermissionAction.READ, 'field.jwt_secret') + const { can: canReadJWTSecret } = useAsyncCheckProjectPermissions( + PermissionAction.READ, + 'field.jwt_secret' + ) const [rlsBypassedWarningDismissed, setRlsBypassedWarningDismissed] = useLocalStorage( LOCAL_STORAGE_KEYS.GRAPHIQL_RLS_BYPASS_WARNING, diff --git a/apps/studio/components/interfaces/Home/NewProjectPanel/APIKeys.tsx b/apps/studio/components/interfaces/Home/NewProjectPanel/APIKeys.tsx index 5c62c1c2a5b..05872a5834f 100644 --- a/apps/studio/components/interfaces/Home/NewProjectPanel/APIKeys.tsx +++ b/apps/studio/components/interfaces/Home/NewProjectPanel/APIKeys.tsx @@ -9,7 +9,7 @@ import Panel from 'components/ui/Panel' import { getKeys, useAPIKeysQuery } from 'data/api-keys/api-keys-query' import { useJwtSecretUpdatingStatusQuery } from 'data/config/jwt-secret-updating-status-query' import { useProjectSettingsV2Query } from 'data/config/project-settings-v2-query' -import { useCheckPermissions } from 'hooks/misc/useCheckPermissions' +import { useAsyncCheckProjectPermissions } from 'hooks/misc/useCheckPermissions' import { Input, SimpleCodeBlock } from 'ui' const generateInitSnippet = (endpoint: string) => ({ @@ -65,7 +65,10 @@ const APIKeys = () => { const jwtSecretUpdateStatus = data?.jwtSecretUpdateStatus - const canReadAPIKeys = useCheckPermissions(PermissionAction.READ, 'service_api_keys') + const { can: canReadAPIKeys } = useAsyncCheckProjectPermissions( + PermissionAction.READ, + 'service_api_keys' + ) const isNotUpdatingJwtSecret = jwtSecretUpdateStatus === undefined || jwtSecretUpdateStatus === JwtSecretUpdateStatus.Updated diff --git a/apps/studio/components/interfaces/Integrations/CronJobs/CreateCronJobSheet.tsx b/apps/studio/components/interfaces/Integrations/CronJobs/CreateCronJobSheet.tsx index bc6dcfa9963..fb97c5c8285 100644 --- a/apps/studio/components/interfaces/Integrations/CronJobs/CreateCronJobSheet.tsx +++ b/apps/studio/components/interfaces/Integrations/CronJobs/CreateCronJobSheet.tsx @@ -16,7 +16,7 @@ import { useDatabaseCronJobCreateMutation } from 'data/database-cron-jobs/databa import { CronJob } from 'data/database-cron-jobs/database-cron-jobs-infinite-query' import { useDatabaseExtensionsQuery } from 'data/database-extensions/database-extensions-query' import { useSendEventMutation } from 'data/telemetry/send-event-mutation' -import { useCheckPermissions } from 'hooks/misc/useCheckPermissions' +import { useAsyncCheckProjectPermissions } from 'hooks/misc/useCheckPermissions' import { useSelectedOrganizationQuery } from 'hooks/misc/useSelectedOrganization' import { useSelectedProjectQuery } from 'hooks/misc/useSelectedProject' import { @@ -220,7 +220,7 @@ export const CreateCronJobSheet = ({ const { mutate: upsertCronJob, isLoading: isUpserting } = useDatabaseCronJobCreateMutation() const isLoading = isLoadingGetCronJob || isUpserting - const canToggleExtensions = useCheckPermissions( + const { can: canToggleExtensions } = useAsyncCheckProjectPermissions( PermissionAction.TENANT_SQL_ADMIN_WRITE, 'extensions' ) diff --git a/apps/studio/components/interfaces/Integrations/GraphQL/GraphiQLTab.tsx b/apps/studio/components/interfaces/Integrations/GraphQL/GraphiQLTab.tsx index 5ea6f0cfb08..2478f55f7cd 100644 --- a/apps/studio/components/interfaces/Integrations/GraphQL/GraphiQLTab.tsx +++ b/apps/studio/components/interfaces/Integrations/GraphQL/GraphiQLTab.tsx @@ -5,14 +5,11 @@ import { useMemo } from 'react' import { toast } from 'sonner' import { useParams } from 'common' -import ExtensionCard from 'components/interfaces/Database/Extensions/ExtensionCard' import GraphiQL from 'components/interfaces/GraphQL/GraphiQL' import { Loading } from 'components/ui/Loading' import { getKeys, useAPIKeysQuery } from 'data/api-keys/api-keys-query' import { useSessionAccessTokenQuery } from 'data/auth/session-access-token-query' import { useProjectPostgrestConfigQuery } from 'data/config/project-postgrest-config-query' -import { useDatabaseExtensionsQuery } from 'data/database-extensions/database-extensions-query' -import { useSelectedProjectQuery } from 'hooks/misc/useSelectedProject' import { API_URL, IS_PLATFORM } from 'lib/constants' import { getRoleImpersonationJWT } from 'lib/role-impersonation' import { useGetImpersonatedRoleState } from 'state/role-impersonation-state' @@ -20,15 +17,8 @@ import { useGetImpersonatedRoleState } from 'state/role-impersonation-state' export const GraphiQLTab = () => { const { resolvedTheme } = useTheme() const { ref: projectRef } = useParams() - const { data: project } = useSelectedProjectQuery() const currentTheme = resolvedTheme?.includes('dark') ? 'dark' : 'light' - const { data, isLoading: isExtensionsLoading } = useDatabaseExtensionsQuery({ - projectRef: project?.ref, - connectionString: project?.connectionString, - }) - const pgGraphqlExtension = (data ?? []).find((ext) => ext.name === 'pg_graphql') - const { data: accessToken } = useSessionAccessTokenQuery({ enabled: IS_PLATFORM }) const { data: apiKeys, isFetched } = useAPIKeysQuery({ projectRef, reveal: true }) @@ -80,29 +70,11 @@ export const GraphiQLTab = () => { } return customFetcher - }, [projectRef, getImpersonatedRoleState, jwtSecret, accessToken, serviceKey]) + }, [projectRef, getImpersonatedRoleState, jwtSecret, accessToken, serviceKey, secretKey?.api_key]) - if ((IS_PLATFORM && !accessToken) || !isFetched || (isExtensionsLoading && !pgGraphqlExtension)) { + if ((IS_PLATFORM && !accessToken) || !isFetched) { return } - if (pgGraphqlExtension?.installed_version === null) { - return ( -
-
-
-

Enable the GraphQL Extension

-

- Toggle the switch below to enable the GraphQL extension. You can then use the GraphQL - API with your Supabase Database. -

-
- - -
-
- ) - } - return } diff --git a/apps/studio/components/interfaces/Integrations/Queues/CreateQueueSheet.tsx b/apps/studio/components/interfaces/Integrations/Queues/CreateQueueSheet.tsx index d09f681b645..b9720548c45 100644 --- a/apps/studio/components/interfaces/Integrations/Queues/CreateQueueSheet.tsx +++ b/apps/studio/components/interfaces/Integrations/Queues/CreateQueueSheet.tsx @@ -74,7 +74,7 @@ const FORM_ID = 'create-queue-sidepanel' export const CreateQueueSheet = ({ isClosing, setIsClosing, onClose }: CreateQueueSheetProps) => { // This is for enabling pg_partman extension which will be used for partitioned queues (3rd kind of queue) // const [showEnableExtensionModal, setShowEnableExtensionModal] = useState(false) - // const canToggleExtensions = useCheckPermissions( + // const { can: canToggleExtensions } = useAsyncCheckPermissions( // PermissionAction.TENANT_SQL_ADMIN_WRITE, // 'extensions' // ) diff --git a/apps/studio/components/interfaces/Integrations/Queues/QueuesSettings.tsx b/apps/studio/components/interfaces/Integrations/Queues/QueuesSettings.tsx index a1ce96787c7..2b5b6bd0792 100644 --- a/apps/studio/components/interfaces/Integrations/Queues/QueuesSettings.tsx +++ b/apps/studio/components/interfaces/Integrations/Queues/QueuesSettings.tsx @@ -21,7 +21,7 @@ import { } from 'data/database-queues/database-queues-toggle-postgrest-mutation' import { useTableUpdateMutation } from 'data/tables/table-update-mutation' import { useTablesQuery } from 'data/tables/tables-query' -import { useCheckPermissions } from 'hooks/misc/useCheckPermissions' +import { useAsyncCheckProjectPermissions } from 'hooks/misc/useCheckPermissions' import { useSelectedProjectQuery } from 'hooks/misc/useSelectedProject' import { Button, @@ -39,7 +39,7 @@ import { FormItemLayout } from 'ui-patterns/form/FormItemLayout/FormItemLayout' export const QueuesSettings = () => { const { data: project } = useSelectedProjectQuery() - const canUpdatePostgrestConfig = useCheckPermissions( + const { can: canUpdatePostgrestConfig } = useAsyncCheckProjectPermissions( PermissionAction.UPDATE, 'custom_config_postgrest' ) diff --git a/apps/studio/components/interfaces/Integrations/Vault/Secrets/SecretRow.tsx b/apps/studio/components/interfaces/Integrations/Vault/Secrets/SecretRow.tsx index 20a1ace90eb..d15afd50960 100644 --- a/apps/studio/components/interfaces/Integrations/Vault/Secrets/SecretRow.tsx +++ b/apps/studio/components/interfaces/Integrations/Vault/Secrets/SecretRow.tsx @@ -6,69 +6,98 @@ import { Button, DropdownMenu, DropdownMenuContent, - DropdownMenuItem, + DropdownMenuSeparator, DropdownMenuTrigger, - Input, - Tooltip, - TooltipContent, - TooltipTrigger, } from 'ui' +import { DropdownMenuItemTooltip } from 'components/ui/DropdownMenuItemTooltip' import { useVaultSecretDecryptedValueQuery } from 'data/vault/vault-secret-decrypted-value-query' -import { useCheckPermissions } from 'hooks/misc/useCheckPermissions' +import { useAsyncCheckProjectPermissions } from 'hooks/misc/useCheckPermissions' import { useSelectedProjectQuery } from 'hooks/misc/useSelectedProject' import { Edit3, Eye, EyeOff, Key, Loader, MoreVertical, Trash } from 'lucide-react' import type { VaultSecret } from 'types' +import { Input } from 'ui-patterns/DataInputs/Input' import EditSecretModal from './EditSecretModal' +import type { SecretTableColumn } from './Secrets.utils' interface SecretRowProps { - secret: VaultSecret + row: VaultSecret + col: SecretTableColumn onSelectRemove: (secret: VaultSecret) => void } -const SecretRow = ({ secret, onSelectRemove }: SecretRowProps) => { +const SecretRow = ({ row, col, onSelectRemove }: SecretRowProps) => { const { ref } = useParams() const { data: project } = useSelectedProjectQuery() const [modal, setModal] = useState(null) const [revealSecret, setRevealSecret] = useState(false) - const name = secret?.name ?? 'No name provided' + const name = row?.name ?? 'No name provided' - const canManageSecrets = useCheckPermissions(PermissionAction.TENANT_SQL_ADMIN_WRITE, 'tables') + const { can: canManageSecrets } = useAsyncCheckProjectPermissions( + PermissionAction.TENANT_SQL_ADMIN_WRITE, + 'tables' + ) const { data: revealedValue, isFetching } = useVaultSecretDecryptedValueQuery( { projectRef: ref!, connectionString: project?.connectionString, - id: secret.id, + id: row.id, }, { - enabled: !!(ref! && secret.id) && revealSecret, + enabled: !!(ref! && row.id) && revealSecret, } ) const onCloseModal = () => setModal(null) - return ( -
-
-
-

- {name} -

- {secret.description !== undefined && ( -

- {secret.description} -

- )} -
-
- -

- {secret.id} -

-
+ if (col.id === 'actions') { + return ( +
e.stopPropagation()}> + + +
-
+ ) + } + + if (col.id === 'secret_value') { + return ( +
e.stopPropagation()}>
+ ) + } + + if (col.id === 'id') { + return ( +
+ +

+ {row.id} +

+
+ ) + } + + return ( +
+

+ {name} +

+ {row.description !== undefined && row.description !== '' && ( +
+

{row.description}

+
+ )}
) } diff --git a/apps/studio/components/interfaces/Integrations/Vault/Secrets/Secrets.utils.tsx b/apps/studio/components/interfaces/Integrations/Vault/Secrets/Secrets.utils.tsx new file mode 100644 index 00000000000..a1bec3a55db --- /dev/null +++ b/apps/studio/components/interfaces/Integrations/Vault/Secrets/Secrets.utils.tsx @@ -0,0 +1,57 @@ +import type { Column } from 'react-data-grid' + +import type { VaultSecret } from 'types' +import { cn } from 'ui' +import SecretRow from './SecretRow' + +export type SecretColumnId = 'secret' | 'id' | 'secret_value' | 'updated_at' | 'actions' + +export interface SecretTableColumn { + id: SecretColumnId + name: string + minWidth?: number + width?: number + maxWidth?: number +} + +export const SECRET_TABLE_COLUMNS: SecretTableColumn[] = [ + { id: 'secret', name: 'Secret', minWidth: 300, width: 360 }, + { id: 'id', name: 'ID', minWidth: 220, width: 260 }, + { id: 'secret_value', name: 'Value', minWidth: 320, width: 420 }, + { id: 'updated_at', name: 'Last updated', minWidth: 180 }, + { id: 'actions', name: '', minWidth: 75, width: 75 }, +] + +export const formatSecretColumns = ({ + onSelectRemove, +}: { + onSelectRemove: (secret: VaultSecret) => void +}): Column[] => { + return SECRET_TABLE_COLUMNS.map((col) => { + const result: Column = { + key: col.id, + name: col.name, + minWidth: col.minWidth ?? 100, + maxWidth: col.maxWidth, + width: col.width, + resizable: false, + sortable: false, + draggable: false, + headerCellClass: undefined, + renderHeaderCell: () => { + return ( +
+

{col.name}

+
+ ) + }, + renderCell: ({ row }) => , + } + return result + }) +} diff --git a/apps/studio/components/interfaces/Integrations/Vault/Secrets/SecretsManagement.tsx b/apps/studio/components/interfaces/Integrations/Vault/Secrets/SecretsManagement.tsx index 43388850602..05bd8a8f726 100644 --- a/apps/studio/components/interfaces/Integrations/Vault/Secrets/SecretsManagement.tsx +++ b/apps/studio/components/interfaces/Integrations/Vault/Secrets/SecretsManagement.tsx @@ -1,28 +1,30 @@ import { PermissionAction } from '@supabase/shared-types/out/constants' +import DataGrid, { Row } from 'react-data-grid' import { sortBy } from 'lodash' -import { Loader, Search, X } from 'lucide-react' -import { Fragment, useEffect, useState } from 'react' +import { Loader, RefreshCw, Search, X } from 'lucide-react' +import { useEffect, useMemo, useState } from 'react' import { useParams } from 'common' import { ButtonTooltip } from 'components/ui/ButtonTooltip' import { DocsButton } from 'components/ui/DocsButton' import { useVaultSecretsQuery } from 'data/vault/vault-secrets-query' -import { useCheckPermissions } from 'hooks/misc/useCheckPermissions' +import { useAsyncCheckProjectPermissions } from 'hooks/misc/useCheckPermissions' import { useSelectedProjectQuery } from 'hooks/misc/useSelectedProject' +import type { VaultSecret } from 'types' import { Button, Input, + LoadingLine, + cn, Select_Shadcn_, - SelectContent_Shadcn_, - SelectItem_Shadcn_, SelectTrigger_Shadcn_, SelectValue_Shadcn_, - Separator, + SelectContent_Shadcn_, + SelectItem_Shadcn_, } from 'ui' import AddNewSecretModal from './AddNewSecretModal' import DeleteSecretModal from './DeleteSecretModal' -import SecretRow from './SecretRow' -import type { VaultSecret } from 'types' +import { formatSecretColumns } from './Secrets.utils' export const SecretsManagement = () => { const { search } = useParams() @@ -31,137 +33,162 @@ export const SecretsManagement = () => { const [searchValue, setSearchValue] = useState('') const [showAddSecretModal, setShowAddSecretModal] = useState(false) const [selectedSecretToRemove, setSelectedSecretToRemove] = useState() - const [selectedSort, setSelectedSort] = useState('updated_at') + const [selectedSort, setSelectedSort] = useState<'updated_at' | 'name'>('updated_at') - const canManageSecrets = useCheckPermissions(PermissionAction.TENANT_SQL_ADMIN_WRITE, 'tables') + const { can: canManageSecrets } = useAsyncCheckProjectPermissions( + PermissionAction.TENANT_SQL_ADMIN_WRITE, + 'tables' + ) + + const { data, isLoading, isRefetching, refetch, error, isError } = useVaultSecretsQuery({ + projectRef: project?.ref!, + connectionString: project?.connectionString, + }) + const allSecrets = useMemo(() => data || [], [data]) + const secrets = useMemo(() => { + const filtered = + searchValue.length > 0 + ? allSecrets.filter( + (secret) => + (secret?.name ?? '').toLowerCase().includes(searchValue.trim().toLowerCase()) || + (secret?.id ?? '').toLowerCase().includes(searchValue.trim().toLowerCase()) + ) + : allSecrets + + if (selectedSort === 'updated_at') { + return sortBy(filtered, (s) => Number(new Date(s.updated_at))).reverse() + } + return sortBy(filtered, (s) => (s.name || '').toLowerCase()) + }, [allSecrets, searchValue, selectedSort]) useEffect(() => { if (search !== undefined) setSearchValue(search) }, [search]) - const { data, isLoading } = useVaultSecretsQuery({ - projectRef: project?.ref!, - connectionString: project?.connectionString, - }) - const allSecrets = data || [] - const secrets = sortBy( - searchValue.length > 0 - ? allSecrets.filter( - (secret) => - (secret?.name ?? '').toLowerCase().includes(searchValue.toLowerCase()) || - (secret?.id ?? '').toLowerCase().includes(searchValue.toLowerCase()) - ) - : allSecrets, - (s) => { - if (selectedSort === 'updated_at') { - return Number(new Date(s.updated_at)) - } else { - return s[selectedSort as keyof VaultSecret] - } - } + const columns = useMemo( + () => + formatSecretColumns({ + onSelectRemove: (secret) => setSelectedSecretToRemove(secret), + }), + [] ) return ( <> -
-
-
- setSearchValue(event.target.value)} - icon={} - actions={ - searchValue.length > 0 - ? [ -
-
- - setShowAddSecretModal(true)} - tooltip={{ - content: { - side: 'bottom', - text: !canManageSecrets - ? 'You need additional permissions to add secrets' - : undefined, - }, - }} - > - Add new secret - -
-
+
+
+
+
+ } + value={searchValue ?? ''} + onChange={(e) => setSearchValue(e.target.value)} + actions={[ + searchValue && ( + + + setShowAddSecretModal(true)} + tooltip={{ + content: { + side: 'bottom', + text: !canManageSecrets + ? 'You need additional permissions to add secrets' + : undefined, + }, + }} + > + Add new secret + +
+
+ + + + {isError ? (
- -

Loading secrets from the Vault

+

Failed to load secrets

) : ( - <> - {secrets.map((secret, idx) => { - return ( - - - {idx !== secrets.length - 1 && } - + row.id} + rowClass={() => { + return cn( + 'cursor-pointer', + '[&>.rdg-cell]:border-box [&>.rdg-cell]:outline-none [&>.rdg-cell]:shadow-none', + '[&>.rdg-cell:first-child>div]:pl-8' ) - })} - {secrets.length === 0 && ( - <> - {searchValue.length === 0 ? ( -
-

No secrets added yet

-

- The Vault allows you to store sensitive information like API keys -

-
- ) : ( -
-

No results found

-

- Your search for "{searchValue}" did not return any results -

-
- )} - - )} - + }} + renderers={{ + renderRow(_, props) { + return + }, + }} + /> )} + + {secrets.length === 0 && !isLoading && !isError ? ( +
+
+

+ {searchValue ? 'No secrets found' : 'No secrets added yet'} +

+

+ {searchValue + ? `There are currently no secrets based on the search "${searchValue}"` + : 'The Vault allows you to store sensitive information like API keys'} +

+
+
+ ) : null}
diff --git a/apps/studio/components/interfaces/Integrations/Webhooks/ListTab.tsx b/apps/studio/components/interfaces/Integrations/Webhooks/ListTab.tsx index 9212fe94801..2e87e67a209 100644 --- a/apps/studio/components/interfaces/Integrations/Webhooks/ListTab.tsx +++ b/apps/studio/components/interfaces/Integrations/Webhooks/ListTab.tsx @@ -1,31 +1,34 @@ +import { PostgresTrigger } from '@supabase/postgres-meta' import { PermissionAction } from '@supabase/shared-types/out/constants' import { useState } from 'react' import DeleteHookModal from 'components/interfaces/Database/Hooks/DeleteHookModal' import { EditHookPanel } from 'components/interfaces/Database/Hooks/EditHookPanel' -import HooksList from 'components/interfaces/Database/Hooks/HooksList/HooksList' +import { HooksList } from 'components/interfaces/Database/Hooks/HooksList/HooksList' import NoPermission from 'components/ui/NoPermission' -import { useCheckPermissions, usePermissionsLoaded } from 'hooks/misc/useCheckPermissions' +import { useAsyncCheckProjectPermissions } from 'hooks/misc/useCheckPermissions' export const WebhooksListTab = () => { const [selectedHook, setSelectedHook] = useState() const [showCreateHookForm, setShowCreateHookForm] = useState(false) const [showDeleteHookForm, setShowDeleteHookForm] = useState(false) - const canReadWebhooks = useCheckPermissions(PermissionAction.TENANT_SQL_ADMIN_READ, 'triggers') - const isPermissionsLoaded = usePermissionsLoaded() + const { can: canReadWebhooks, isSuccess: isPermissionsLoaded } = useAsyncCheckProjectPermissions( + PermissionAction.TENANT_SQL_ADMIN_READ, + 'triggers' + ) const createHook = () => { setSelectedHook(undefined) setShowCreateHookForm(true) } - const editHook = (hook: any) => { + const editHook = (hook: PostgresTrigger) => { setSelectedHook(hook) setShowCreateHookForm(true) } - const deleteHook = (hook: any) => { + const deleteHook = (hook: PostgresTrigger) => { setSelectedHook(hook) setShowDeleteHookForm(true) } diff --git a/apps/studio/components/interfaces/Integrations/Webhooks/OverviewTab.tsx b/apps/studio/components/interfaces/Integrations/Webhooks/OverviewTab.tsx index 949180c05e8..a1efc6d7310 100644 --- a/apps/studio/components/interfaces/Integrations/Webhooks/OverviewTab.tsx +++ b/apps/studio/components/interfaces/Integrations/Webhooks/OverviewTab.tsx @@ -7,7 +7,7 @@ import NoPermission from 'components/ui/NoPermission' import { GenericSkeletonLoader } from 'components/ui/ShimmeringLoader' import { useHooksEnableMutation } from 'data/database/hooks-enable-mutation' import { useSchemasQuery } from 'data/database/schemas-query' -import { useCheckPermissions, usePermissionsLoaded } from 'hooks/misc/useCheckPermissions' +import { useAsyncCheckProjectPermissions } from 'hooks/misc/useCheckPermissions' import { useSelectedProjectQuery } from 'hooks/misc/useSelectedProject' import { Admonition } from 'ui-patterns' import { IntegrationOverviewTab } from '../Integration/IntegrationOverviewTab' @@ -26,8 +26,10 @@ export const WebhooksOverviewTab = () => { }) const isHooksEnabled = schemas?.some((schema) => schema.name === 'supabase_functions') - const canReadWebhooks = useCheckPermissions(PermissionAction.TENANT_SQL_ADMIN_READ, 'triggers') - const isPermissionsLoaded = usePermissionsLoaded() + const { can: canReadWebhooks, isLoading: isLoadingPermissions } = useAsyncCheckProjectPermissions( + PermissionAction.TENANT_SQL_ADMIN_READ, + 'triggers' + ) const { mutate: enableHooks, isLoading: isEnablingHooks } = useHooksEnableMutation({ onSuccess: async () => { @@ -41,18 +43,18 @@ export const WebhooksOverviewTab = () => { enableHooks({ ref: projectRef }) } - if (isPermissionsLoaded && !canReadWebhooks) { + if (!isSchemasLoaded || isLoadingPermissions) { return (
- +
) } - if (!isSchemasLoaded) { + if (!canReadWebhooks) { return (
- +
) } @@ -73,14 +75,13 @@ export const WebhooksOverviewTab = () => { enableHooksForProject()} - disabled={!isPermissionsLoaded || isEnablingHooks} + disabled={isEnablingHooks} tooltip={{ content: { side: 'bottom', - text: - isPermissionsLoaded && !canReadWebhooks - ? 'You need additional permissions to enable webhooks' - : undefined, + text: !canReadWebhooks + ? 'You need additional permissions to enable webhooks' + : undefined, }, }} > diff --git a/apps/studio/components/interfaces/Integrations/Wrappers/OverviewTab.tsx b/apps/studio/components/interfaces/Integrations/Wrappers/OverviewTab.tsx index f9ab42c19eb..dd923c8c728 100644 --- a/apps/studio/components/interfaces/Integrations/Wrappers/OverviewTab.tsx +++ b/apps/studio/components/interfaces/Integrations/Wrappers/OverviewTab.tsx @@ -5,7 +5,7 @@ import { useState } from 'react' import { useParams } from 'common' import { ButtonTooltip } from 'components/ui/ButtonTooltip' import { useDatabaseExtensionsQuery } from 'data/database-extensions/database-extensions-query' -import { useCheckPermissions } from 'hooks/misc/useCheckPermissions' +import { useAsyncCheckProjectPermissions } from 'hooks/misc/useCheckPermissions' import { useSelectedProjectQuery } from 'hooks/misc/useSelectedProject' import { Alert_Shadcn_, @@ -27,7 +27,11 @@ export const WrapperOverviewTab = () => { const { data: project } = useSelectedProjectQuery() const [createWrapperShown, setCreateWrapperShown] = useState(false) const [isClosingCreateWrapper, setisClosingCreateWrapper] = useState(false) - const canCreateWrapper = useCheckPermissions(PermissionAction.TENANT_SQL_ADMIN_WRITE, 'wrappers') + + const { can: canCreateWrapper } = useAsyncCheckProjectPermissions( + PermissionAction.TENANT_SQL_ADMIN_WRITE, + 'wrappers' + ) const { data } = useDatabaseExtensionsQuery({ projectRef: project?.ref, diff --git a/apps/studio/components/interfaces/Integrations/Wrappers/WrapperRow.tsx b/apps/studio/components/interfaces/Integrations/Wrappers/WrapperRow.tsx index e7c9c155fbe..b79a47aa275 100644 --- a/apps/studio/components/interfaces/Integrations/Wrappers/WrapperRow.tsx +++ b/apps/studio/components/interfaces/Integrations/Wrappers/WrapperRow.tsx @@ -7,7 +7,7 @@ import { useState } from 'react' import { useParams } from 'common' import { ButtonTooltip } from 'components/ui/ButtonTooltip' import type { FDW } from 'data/fdw/fdws-query' -import { useCheckPermissions } from 'hooks/misc/useCheckPermissions' +import { useAsyncCheckProjectPermissions } from 'hooks/misc/useCheckPermissions' import { Badge, Sheet, SheetContent, TableCell, TableRow } from 'ui' import { INTEGRATIONS } from '../Landing/Integrations.constants' import DeleteWrapperModal from './DeleteWrapperModal' @@ -20,7 +20,10 @@ interface WrapperRowProps { const WrapperRow = ({ wrapper }: WrapperRowProps) => { const { ref, id } = useParams() - const canManageWrappers = useCheckPermissions(PermissionAction.TENANT_SQL_ADMIN_WRITE, 'wrappers') + const { can: canManageWrappers } = useAsyncCheckProjectPermissions( + PermissionAction.TENANT_SQL_ADMIN_WRITE, + 'wrappers' + ) const [editWrapperShown, setEditWrapperShown] = useState(false) const [isClosingEditWrapper, setIsClosingEditWrapper] = useState(false) diff --git a/apps/studio/components/interfaces/Integrations/Wrappers/WrappersTab.tsx b/apps/studio/components/interfaces/Integrations/Wrappers/WrappersTab.tsx index f7367a370cc..f9669f4a8ab 100644 --- a/apps/studio/components/interfaces/Integrations/Wrappers/WrappersTab.tsx +++ b/apps/studio/components/interfaces/Integrations/Wrappers/WrappersTab.tsx @@ -4,7 +4,7 @@ import { HTMLProps, ReactNode, useCallback, useState } from 'react' import { useParams } from 'common' import { ButtonTooltip } from 'components/ui/ButtonTooltip' import { FDW, useFDWsQuery } from 'data/fdw/fdws-query' -import { useCheckPermissions } from 'hooks/misc/useCheckPermissions' +import { useAsyncCheckProjectPermissions } from 'hooks/misc/useCheckPermissions' import { useSelectedProjectQuery } from 'hooks/misc/useSelectedProject' import { Sheet, SheetContent } from 'ui' import { CreateWrapperSheet } from './CreateWrapperSheet' @@ -19,7 +19,11 @@ export const WrappersTab = () => { const [selectedWrapperForDelete, setSelectedWrapperForDelete] = useState(null) const [createWrapperShown, setCreateWrapperShown] = useState(false) const [isClosingCreateWrapper, setisClosingCreateWrapper] = useState(false) - const canCreateWrapper = useCheckPermissions(PermissionAction.TENANT_SQL_ADMIN_WRITE, 'wrappers') + + const { can: canCreateWrapper } = useAsyncCheckProjectPermissions( + PermissionAction.TENANT_SQL_ADMIN_WRITE, + 'wrappers' + ) const { data } = useFDWsQuery({ projectRef: project?.ref, diff --git a/apps/studio/components/interfaces/JwtSecrets/jwt-settings.tsx b/apps/studio/components/interfaces/JwtSecrets/jwt-settings.tsx index 49c06672fc3..5df90f4e495 100644 --- a/apps/studio/components/interfaces/JwtSecrets/jwt-settings.tsx +++ b/apps/studio/components/interfaces/JwtSecrets/jwt-settings.tsx @@ -15,7 +15,7 @@ import { useJwtSecretUpdateMutation } from 'data/config/jwt-secret-update-mutati import { useJwtSecretUpdatingStatusQuery } from 'data/config/jwt-secret-updating-status-query' import { useProjectPostgrestConfigQuery } from 'data/config/project-postgrest-config-query' import { useLegacyJWTSigningKeyQuery } from 'data/jwt-signing-keys/legacy-jwt-signing-key-query' -import { useCheckPermissions } from 'hooks/misc/useCheckPermissions' +import { useAsyncCheckProjectPermissions } from 'hooks/misc/useCheckPermissions' import { useFlag } from 'hooks/ui/useFlag' import { uuidv4 } from 'lib/helpers' import { @@ -73,12 +73,18 @@ const JWTSettings = () => { const [isCreatingKey, setIsCreatingKey] = useState(false) const [isRegeneratingKey, setIsGeneratingKey] = useState(false) - const canReadJWTSecret = useCheckPermissions(PermissionAction.READ, 'field.jwt_secret') - const canGenerateNewJWTSecret = useCheckPermissions( + const { can: canReadJWTSecret } = useAsyncCheckProjectPermissions( + PermissionAction.READ, + 'field.jwt_secret' + ) + const { can: canGenerateNewJWTSecret } = useAsyncCheckProjectPermissions( PermissionAction.INFRA_EXECUTE, 'queue_job.projects.update_jwt' ) - const canUpdateConfig = useCheckPermissions(PermissionAction.UPDATE, 'custom_config_gotrue') + const { can: canUpdateConfig } = useAsyncCheckProjectPermissions( + PermissionAction.UPDATE, + 'custom_config_gotrue' + ) const { data } = useJwtSecretUpdatingStatusQuery({ projectRef }) const { data: config, isError } = useProjectPostgrestConfigQuery({ projectRef }) diff --git a/apps/studio/components/interfaces/Linter/LinterPageFooter.tsx b/apps/studio/components/interfaces/Linter/LinterPageFooter.tsx index 10f5361f10c..65a072ac9ed 100644 --- a/apps/studio/components/interfaces/Linter/LinterPageFooter.tsx +++ b/apps/studio/components/interfaces/Linter/LinterPageFooter.tsx @@ -21,7 +21,7 @@ const LinterPageFooter = ({ >

Reset suggestions

- Consider resetting the analysis making any changes + Consider resetting the analysis after making any changes

+
+
+ + + + ) +} + +export default AwsMarketplaceCreateNewOrg diff --git a/apps/studio/components/interfaces/Organization/CloudMarketplace/AwsMarketplaceLinkExistingOrg.tsx b/apps/studio/components/interfaces/Organization/CloudMarketplace/AwsMarketplaceLinkExistingOrg.tsx new file mode 100644 index 00000000000..c00b2b3fa0e --- /dev/null +++ b/apps/studio/components/interfaces/Organization/CloudMarketplace/AwsMarketplaceLinkExistingOrg.tsx @@ -0,0 +1,342 @@ +import { + Button, + Collapsible_Shadcn_, + CollapsibleContent_Shadcn_, + CollapsibleTrigger_Shadcn_, + Form_Shadcn_, + FormField_Shadcn_, + Skeleton, +} from 'ui' +import { RadioGroupCard, RadioGroupCardItem } from '@ui/components/radio-group-card' +import { FormItemLayout } from 'ui-patterns/form/FormItemLayout/FormItemLayout' +import { cn } from '@ui/lib/utils' +import { ActionCard } from '../../../ui/ActionCard' +import { Boxes, ChevronRight } from 'lucide-react' +import { + ScaffoldSection, + ScaffoldSectionContent, + ScaffoldSectionDetail, +} from '../../../layouts/Scaffold' +import { ButtonTooltip } from '../../../ui/ButtonTooltip' +import { z } from 'zod' +import { SubmitHandler, useForm } from 'react-hook-form' +import { zodResolver } from '@hookform/resolvers/zod' +import { useMemo, useState } from 'react' +import { Organization } from '../../../../types' +import { useProjectsQuery } from '../../../../data/projects/projects-query' +import { useOrganizationLinkAwsMarketplaceMutation } from '../../../../data/organizations/organization-link-aws-marketplace-mutation' +import { toast } from 'sonner' +import AwsMarketplaceOnboardingSuccessModal from './AwsMarketplaceOnboardingSuccessModal' +import NewAwsMarketplaceOrgModal from './NewAwsMarketplaceOrgModal' +import { useRouter } from 'next/router' +import AwsMarketplaceAutoRenewalWarning from './AwsMarketplaceAutoRenewalWarning' +import { CloudMarketplaceOnboardingInfo } from './cloud-marketplace-query' +import Link from 'next/link' + +interface Props { + organizations?: Organization[] | undefined + onboardingInfo?: CloudMarketplaceOnboardingInfo | undefined + isLoadingOnboardingInfo: boolean +} + +const FormSchema = z.object({ + orgSlug: z.string(), +}) + +export type LinkExistingOrgForm = z.infer + +const AwsMarketplaceLinkExistingOrg = ({ + organizations, + onboardingInfo, + isLoadingOnboardingInfo, +}: Props) => { + const router = useRouter() + const { + query: { buyer_id: buyerId }, + } = router + + const form = useForm({ + resolver: zodResolver(FormSchema), + defaultValues: { + orgSlug: undefined, + }, + mode: 'onBlur', + reValidateMode: 'onChange', + }) + + const isDirty = !!Object.keys(form.formState.dirtyFields).length + + // Sort organizations by name ascending + const sortedOrganizations = useMemo(() => { + return organizations?.slice().sort((a, b) => a.name.localeCompare(b.name)) + }, [organizations]) + + const { orgsLinkable, orgsNotLinkable } = useMemo(() => { + const orgQualifiesForLinking = (org: Organization) => { + const validationResult = onboardingInfo?.organization_linking_eligibility.find( + (result) => result.slug === org.slug + ) + + return validationResult?.is_eligible ?? false + } + + const linkable: Organization[] = [] + const notLinkable: Organization[] = [] + sortedOrganizations?.forEach((org) => { + if (orgQualifiesForLinking(org)) { + linkable.push(org) + } else { + notLinkable.push(org) + } + }) + return { orgsLinkable: linkable, orgsNotLinkable: notLinkable } + }, [sortedOrganizations, onboardingInfo?.organization_linking_eligibility]) + + const { data: projects = [] } = useProjectsQuery() + + const [isNotLinkableOrgListOpen, setIsNotLinkableOrgListOpen] = useState(false) + const [orgLinkedSuccessfully, setOrgLinkedSuccessfully] = useState(false) + const [showOrgCreationDialog, setShowOrgCreationDialog] = useState(false) + const [orgToRedirectTo, setOrgToRedirectTo] = useState('') + + const { mutate: linkOrganization, isLoading: isLinkingOrganization } = + useOrganizationLinkAwsMarketplaceMutation({ + onSuccess: (_) => { + //TODO(thomas): send tracking event? + setOrgLinkedSuccessfully(true) + setOrgToRedirectTo(form.getValues('orgSlug')) + }, + onError: (res) => { + toast.error(res.message, { + duration: 7_000, + }) + }, + }) + + const onSubmit: SubmitHandler = async (values) => { + linkOrganization({ slug: values.orgSlug, buyerId: buyerId as string }) + } + + return ( + <> + {onboardingInfo && !onboardingInfo.aws_contract_auto_renewal && ( + + )} + + + <> +

+ You’ve subscribed to the Supabase {onboardingInfo?.plan_name_selected_on_marketplace}{' '} + Plan via the AWS Marketplace. As a final step, you need to link a Supabase + organization to that subscription. Select the organization you want to be managed and + billed through AWS. +

+ +

+ You can read more on billing through AWS in our {''} + {/*TODO(thomas): Update docs link once the new docs exist*/} + + Billing Docs. + +

+ +

+ Want to start fresh? Create a + new organization and it will be linked automatically. +

+ + +
+ + + +
+ ( + { + form.setValue('orgSlug', value, { + shouldDirty: true, + shouldValidate: false, + }) + }} + > + +
+ {isLoadingOnboardingInfo ? ( + Array(3) + .fill(0) + .map((_, i) => ( + + )) + ) : ( + <> +

+ Organizations that can be linked +

+ {orgsLinkable.length === 0 ? ( +

+ None of your organizations can be linked to your AWS Marketplace + subscription at the moment. +

+ ) : ( + <> + {orgsLinkable.map((org) => { + const numProjects = projects.filter( + (p) => p.organization_slug === org.slug + ).length + return ( + + } + title={org.name} + description={`${org.plan.name} Plan • ${numProjects > 0 ? `${numProjects} Project${numProjects > 1 ? 's' : ''}` : '0 Projects'}`} + /> + } + /> + ) + })} + + )} + + )} +
+
+
+ )} + /> + +
+ + {orgsNotLinkable.length > 0 && !isLoadingOnboardingInfo && ( + setIsNotLinkableOrgListOpen((prev) => !prev)} + > + +

+ Organizations that can't be linked +

+ +
+ +

+ The following organizations can’t be linked to your AWS Marketplace subscription + at the moment. This may be due to missing permissions, outstanding invoices, or an + existing marketplace link. If you'd like to link one of these organizations, + please review the organization settings. You need to be Owner or Administrator of + the organization to link it. +

+
+ {orgsNotLinkable.map((org) => { + const numProjects = projects.filter( + (p) => p.organization_slug === org.slug + ).length + return ( + } + title={org.name} + description={`${org.plan.name} Plan • ${numProjects > 0 ? `${numProjects} Project${numProjects > 1 ? 's' : ''}` : '0 Projects'}`} + /> + ) + })} +
+
+
+ )} + +
+ { + await onSubmit(form.getValues()) + }} + loading={isLinkingOrganization} + disabled={!isDirty || isLinkingOrganization || isLoadingOnboardingInfo} + tooltip={{ + content: { + side: 'top', + text: !isDirty ? 'No organization selected' : undefined, + }, + }} + > + Link organization + +
+
+
+ + { + setOrgLinkedSuccessfully(false) + router.push(`/org/${orgToRedirectTo}`) + }} + /> + + setShowOrgCreationDialog(false)} + buyerId={buyerId as string} + onSuccess={(newlyCreatedOrgSlug) => { + setShowOrgCreationDialog(false) + setOrgToRedirectTo(newlyCreatedOrgSlug) + setOrgLinkedSuccessfully(true) + }} + /> + + ) +} + +export default AwsMarketplaceLinkExistingOrg diff --git a/apps/studio/components/interfaces/Organization/CloudMarketplace/AwsMarketplaceOnboardingPlaceholder.tsx b/apps/studio/components/interfaces/Organization/CloudMarketplace/AwsMarketplaceOnboardingPlaceholder.tsx new file mode 100644 index 00000000000..07f9c13abe4 --- /dev/null +++ b/apps/studio/components/interfaces/Organization/CloudMarketplace/AwsMarketplaceOnboardingPlaceholder.tsx @@ -0,0 +1,29 @@ +import { + ScaffoldSection, + ScaffoldSectionContent, + ScaffoldSectionDetail, +} from '../../../layouts/Scaffold' +import { Skeleton } from '@ui/components/shadcn/ui/skeleton' + +const AwsMarketplaceOnboardingPlaceholder = () => { + return ( + + + {Array(1) + .fill(0) + .map((_, i) => ( + + ))} + + + {Array(3) + .fill(0) + .map((_, i) => ( + + ))} + + + ) +} + +export default AwsMarketplaceOnboardingPlaceholder diff --git a/apps/studio/components/interfaces/Organization/CloudMarketplace/AwsMarketplaceOnboardingSuccessModal.tsx b/apps/studio/components/interfaces/Organization/CloudMarketplace/AwsMarketplaceOnboardingSuccessModal.tsx new file mode 100644 index 00000000000..1402382228b --- /dev/null +++ b/apps/studio/components/interfaces/Organization/CloudMarketplace/AwsMarketplaceOnboardingSuccessModal.tsx @@ -0,0 +1,42 @@ +import { Dialog, DialogContent, DialogFooter, DialogSection } from '@ui/components/shadcn/ui/dialog' +import { Button } from 'ui' + +interface Props { + visible: boolean + onClose: () => void +} + +const AwsMarketplaceOnboardingSuccessModal = ({ visible, onClose }: Props) => { + return ( + { + if (!open) onClose() + }} + > + event.preventDefault()} + size="xlarge" + hideClose={true} + onEscapeKeyDown={(e) => e.preventDefault()} + onPointerDownOutside={(e) => e.preventDefault()} + > + +
+

AWS Marketplace Setup completed

+

+ The organization is now managed and billed through AWS Marketplace. +

+
+
+ + + +
+
+ ) +} + +export default AwsMarketplaceOnboardingSuccessModal diff --git a/apps/studio/components/interfaces/Organization/CloudMarketplace/NewAwsMarketplaceOrgForm.tsx b/apps/studio/components/interfaces/Organization/CloudMarketplace/NewAwsMarketplaceOrgForm.tsx new file mode 100644 index 00000000000..cc27b6389c3 --- /dev/null +++ b/apps/studio/components/interfaces/Organization/CloudMarketplace/NewAwsMarketplaceOrgForm.tsx @@ -0,0 +1,159 @@ +import { z } from 'zod' +import { + Form_Shadcn_, + FormControl_Shadcn_, + FormField_Shadcn_, + Input_Shadcn_, + Label_Shadcn_, + Select_Shadcn_, + SelectContent_Shadcn_, + SelectItem_Shadcn_, + SelectTrigger_Shadcn_, + SelectValue_Shadcn_, +} from 'ui' +import { FormItemLayout } from 'ui-patterns/form/FormItemLayout/FormItemLayout' +import { useForm } from 'react-hook-form' +import { zodResolver } from '@hookform/resolvers/zod' + +const ORG_KIND_TYPES = { + PERSONAL: 'Personal', + EDUCATIONAL: 'Educational', + STARTUP: 'Startup', + AGENCY: 'Agency', + COMPANY: 'Company', + UNDISCLOSED: 'N/A', +} + +const ORG_KIND_DEFAULT = 'PERSONAL' + +const ORG_SIZE_TYPES = { + '1': '1 - 10', + '10': '10 - 49', + '50': '50 - 99', + '100': '100 - 299', + '300': 'More than 300', +} + +interface Props { + onSubmit: (values: NewMarketplaceOrgForm) => void +} + +export const CREATE_AWS_MANAGED_ORG_FORM_ID = 'create-aws-managed-org-form' + +const FormSchema = z.object({ + name: z.string().trim().min(1, 'Please provide an organization name'), + kind: z.string(), + size: z.string().optional(), +}) + +export type NewMarketplaceOrgForm = z.infer + +const NewAwsMarketplaceOrgForm = ({ onSubmit }: Props) => { + const form = useForm({ + resolver: zodResolver(FormSchema), + defaultValues: { + name: '', + kind: ORG_KIND_DEFAULT, + }, + }) + + const kind = form.watch('kind') + + return ( + +
+
+ ( + + + <> + +
+ + What's the name of your company or team? + +
+ +
+
+ )} + /> + ( + + + <> + + + + + + {Object.entries(ORG_KIND_TYPES).map(([k, v]) => ( + + {v} + + ))} + + +
+ + What would best describe your organization? + +
+ +
+
+ )} + /> + {kind == 'COMPANY' && ( + ( + + + <> + + + + + + {Object.entries(ORG_SIZE_TYPES).map(([k, v]) => ( + + {v} + + ))} + + +
+ + How many people are in your company? + +
+ +
+
+ )} + /> + )} +
+
+
+ ) +} + +export default NewAwsMarketplaceOrgForm diff --git a/apps/studio/components/interfaces/Organization/CloudMarketplace/NewAwsMarketplaceOrgModal.tsx b/apps/studio/components/interfaces/Organization/CloudMarketplace/NewAwsMarketplaceOrgModal.tsx new file mode 100644 index 00000000000..13a31c4be60 --- /dev/null +++ b/apps/studio/components/interfaces/Organization/CloudMarketplace/NewAwsMarketplaceOrgModal.tsx @@ -0,0 +1,84 @@ +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogSection, + DialogSectionSeparator, + DialogTitle, +} from '@ui/components/shadcn/ui/dialog' +import { Button } from 'ui' +import NewAwsMarketplaceOrgForm, { + CREATE_AWS_MANAGED_ORG_FORM_ID, + NewMarketplaceOrgForm, +} from './NewAwsMarketplaceOrgForm' +import { useAwsManagedOrganizationCreateMutation } from '../../../../data/organizations/organization-create-mutation' +import { toast } from 'sonner' +import { SubmitHandler } from 'react-hook-form' + +interface Props { + buyerId: string + visible: boolean + onSuccess: (newlyCreatedOrgSlug: string) => void + onClose: () => void +} + +const NewAwsMarketplaceOrgModal = ({ buyerId, visible, onSuccess, onClose }: Props) => { + const { mutate: createOrganization, isLoading: isCreatingOrganization } = + useAwsManagedOrganizationCreateMutation({ + onSuccess: (org) => { + //TODO(thomas): send tracking event? + onSuccess(org.slug) + }, + onError: (res) => { + toast.error(res.message, { + duration: 7_000, + }) + }, + }) + + const onSubmit: SubmitHandler = async (values) => { + createOrganization({ ...values, buyerId }) + } + + return ( + { + if (!open) onClose() + }} + > + event.preventDefault()} + size="xlarge" + onEscapeKeyDown={(e) => (isCreatingOrganization ? e.preventDefault() : onClose())} + onPointerDownOutside={(e) => (isCreatingOrganization ? e.preventDefault() : onClose())} + className="p-2" + > + + Create a new organization + + A new organization will be created and linked to your AWS Marketplace subscription + + + + + + + + + + + + ) +} + +export default NewAwsMarketplaceOrgModal diff --git a/apps/studio/components/interfaces/Organization/CloudMarketplace/cloud-marketplace-query.ts b/apps/studio/components/interfaces/Organization/CloudMarketplace/cloud-marketplace-query.ts new file mode 100644 index 00000000000..0b9d924aa4c --- /dev/null +++ b/apps/studio/components/interfaces/Organization/CloudMarketplace/cloud-marketplace-query.ts @@ -0,0 +1,50 @@ +import { get, handleError } from '../../../../data/fetchers' +import type { ResponseError } from '../../../../types' +import { useQuery, UseQueryOptions } from '@tanstack/react-query' +import { useProfile } from '../../../../lib/profile' +import { cloudMarketplaceKeys } from './keys' + +export type CloudMarketplaceOnboardingInfoVariables = { + buyerId: string +} + +export async function getCloudMarketplaceOnboardingInfo( + { buyerId }: CloudMarketplaceOnboardingInfoVariables, + signal?: AbortSignal +) { + const { data, error } = await get( + '/platform/cloud-marketplace/buyers/{buyer_id}/onboarding-info', + { + params: { path: { buyer_id: buyerId } }, + signal, + } + ) + + if (error) handleError(error) + + return data +} + +export type CloudMarketplaceOnboardingInfo = Awaited< + ReturnType +> +export type CloudMarketplaceOnboardingInfoError = ResponseError + +export const useCloudMarketplaceOnboardingInfoQuery = ( + { buyerId }: CloudMarketplaceOnboardingInfoVariables, + { + enabled = true, + ...options + }: UseQueryOptions< + CloudMarketplaceOnboardingInfo, + CloudMarketplaceOnboardingInfoError, + TData + > = {} +) => { + const { profile } = useProfile() + return useQuery( + cloudMarketplaceKeys.onboardingInfo(buyerId), + ({ signal }) => getCloudMarketplaceOnboardingInfo({ buyerId }, signal), + { enabled: enabled && profile !== undefined, ...options, staleTime: 30 * 60 * 1000 } + ) +} diff --git a/apps/studio/components/interfaces/Organization/CloudMarketplace/keys.ts b/apps/studio/components/interfaces/Organization/CloudMarketplace/keys.ts new file mode 100644 index 00000000000..790b1e13329 --- /dev/null +++ b/apps/studio/components/interfaces/Organization/CloudMarketplace/keys.ts @@ -0,0 +1,3 @@ +export const cloudMarketplaceKeys = { + onboardingInfo: (buyerId: string) => ['cloud-marketplace', 'onboarding-info', buyerId], +} diff --git a/apps/studio/components/interfaces/Organization/TeamSettings/MemberRow.tsx b/apps/studio/components/interfaces/Organization/TeamSettings/MemberRow.tsx index ea047d76d18..ce683309f80 100644 --- a/apps/studio/components/interfaces/Organization/TeamSettings/MemberRow.tsx +++ b/apps/studio/components/interfaces/Organization/TeamSettings/MemberRow.tsx @@ -1,7 +1,6 @@ import { ArrowRight, Check, Minus, User, X } from 'lucide-react' import Link from 'next/link' -import Table from 'components/to-be-cleaned/Table' import PartnerIcon from 'components/ui/PartnerIcon' import { ProfileImage } from 'components/ui/ProfileImage' import { useOrganizationRolesV2Query } from 'data/organization-members/organization-roles-query' @@ -17,6 +16,8 @@ import { HoverCard_Shadcn_, ScrollArea, cn, + TableRow, + TableCell, } from 'ui' import ShimmeringLoader from 'ui-patterns/ShimmeringLoader' import { isInviteExpired } from '../Organization.utils' @@ -68,8 +69,8 @@ export const MemberRow = ({ member }: MemberRowProps) => { }).length > 0 return ( - - + +
{ /> )}
-
+ - + {isInvitedUser && member.invited_at && ( {isInviteExpired(member.invited_at) ? 'Expired' : 'Invited'} )} {member.is_sso_user && SSO} - + - +
{member.mfa_enabled ? ( @@ -122,9 +123,9 @@ export const MemberRow = ({ member }: MemberRowProps) => { )}
-
+ - + {isLoadingRoles ? ( ) : isObfuscated ? ( @@ -201,11 +202,11 @@ export const MemberRow = ({ member }: MemberRowProps) => { ) }) )} - + - + - -
+ + ) } diff --git a/apps/studio/components/interfaces/Organization/TeamSettings/MembersView.tsx b/apps/studio/components/interfaces/Organization/TeamSettings/MembersView.tsx index 3ecc3b3de90..22b3d069982 100644 --- a/apps/studio/components/interfaces/Organization/TeamSettings/MembersView.tsx +++ b/apps/studio/components/interfaces/Organization/TeamSettings/MembersView.tsx @@ -1,7 +1,6 @@ import { AlertCircle, HelpCircle } from 'lucide-react' import { useParams } from 'common' -import Table from 'components/to-be-cleaned/Table' import AlertError from 'components/ui/AlertError' import { GenericSkeletonLoader } from 'components/ui/ShimmeringLoader' import { useOrganizationRolesV2Query } from 'data/organization-members/organization-roles-query' @@ -9,7 +8,20 @@ import { useOrganizationMembersQuery } from 'data/organizations/organization-mem import { useProfile } from 'lib/profile' import { partition } from 'lodash' import { useMemo } from 'react' -import { Button, Loading, Tooltip, TooltipContent, TooltipTrigger } from 'ui' +import { + Button, + Loading, + Tooltip, + TooltipContent, + TooltipTrigger, + Table, + TableHeader, + TableHead, + TableBody, + TableCell, + TableRow, + Card, +} from 'ui' import { Admonition } from 'ui-patterns' import { MemberRow } from './MemberRow' @@ -78,77 +90,86 @@ const MembersView = ({ searchString }: MembersViewProps) => { {isSuccessMembers && (
- - User, - , - - Enabled MFA - , - - Role - - - - - How to configure access control? - - , - , - ]} - body={[ - ...(isSuccessRoles && isSuccessMembers && !isOrgScopedRole - ? [ - - - - - , - ] - : []), - ...(!!user ? [] : []), - ...sortedMembers.map((member) => ( - - )), - ...(searchString.length > 0 && filteredMembers.length === 0 - ? [ - - -
- -

- No users matched the search query "{searchString}" -

-
-
-
, - ] - : []), - - -

- {searchString ? `${filteredMembers.length} of ` : ''} - {members.length || '0'} {members.length == 1 ? 'user' : 'users'} -

-
-
, - ]} - /> - + + +
+ + + User + + + Enabled MFA + + + Role + + + + + + How to configure access control? + + + + + + + + + {[ + ...(isSuccessRoles && isSuccessMembers && !isOrgScopedRole + ? [ + + + + + , + ] + : []), + ...(!!user ? [] : []), + ...sortedMembers.map((member) => ( + + )), + ...(searchString.length > 0 && filteredMembers.length === 0 + ? [ + + +
+ +

+ No users matched the search query "{searchString}" +

+
+
+
, + ] + : []), + + +

+ {searchString ? `${filteredMembers.length} of ` : ''} + {members.length || '0'} {members.length == 1 ? 'user' : 'users'} +

+
+
, + ]} +
+
+
+
)} diff --git a/apps/studio/components/interfaces/Organization/Usage/Bandwidth.tsx b/apps/studio/components/interfaces/Organization/Usage/Egress.tsx similarity index 71% rename from apps/studio/components/interfaces/Organization/Usage/Bandwidth.tsx rename to apps/studio/components/interfaces/Organization/Usage/Egress.tsx index f6bbf70669b..0ce4d8e2d9e 100644 --- a/apps/studio/components/interfaces/Organization/Usage/Bandwidth.tsx +++ b/apps/studio/components/interfaces/Organization/Usage/Egress.tsx @@ -3,7 +3,7 @@ import { PricingMetric, useOrgDailyStatsQuery } from 'data/analytics/org-daily-s import type { OrgSubscription } from 'data/subscriptions/types' import UsageSection from './UsageSection/UsageSection' -export interface BandwidthProps { +export interface EgressProps { orgSlug: string projectRef?: string startDate: string | undefined @@ -12,14 +12,14 @@ export interface BandwidthProps { currentBillingCycleSelected: boolean } -const Bandwidth = ({ +const Egress = ({ orgSlug, projectRef, subscription, startDate, endDate, currentBillingCycleSelected, -}: BandwidthProps) => { +}: EgressProps) => { const { data: egressData, isLoading: isLoadingDbEgressData } = useOrgDailyStatsQuery({ orgSlug, projectRef, @@ -29,6 +29,15 @@ const Bandwidth = ({ endDate, }) + const { data: cachedEgressData, isLoading: isLoadingCachedEgress } = useOrgDailyStatsQuery({ + orgSlug, + projectRef, + metric: PricingMetric.CACHED_EGRESS, + interval: '1d', + startDate, + endDate, + }) + const chartMeta: { [key: string]: { data: DataPoint[]; margin: number; isLoading: boolean } } = { @@ -37,13 +46,18 @@ const Bandwidth = ({ margin: 16, isLoading: isLoadingDbEgressData, }, + [PricingMetric.CACHED_EGRESS]: { + data: cachedEgressData?.data ?? [], + margin: 16, + isLoading: isLoadingCachedEgress, + }, } return ( JSX.Element | null } -export type CategoryMetaKey = 'bandwidth' | 'sizeCount' | 'activity' | 'compute' +export type CategoryMetaKey = 'egress' | 'sizeCount' | 'activity' | 'compute' export interface CategoryMeta { key: CategoryMetaKey @@ -72,249 +72,285 @@ export interface CategoryMeta { export const USAGE_CATEGORIES: (subscription?: OrgSubscription) => CategoryMeta[] = ( subscription -) => [ - { - key: 'bandwidth', - name: 'Bandwidth', - description: 'Amount of data transmitted over all network connections', - attributes: [ - { - anchor: 'egress', - key: PricingMetric.EGRESS, - attributes: [ - { key: EgressType.AUTH, name: 'Auth Egress', color: 'yellow' }, - { key: EgressType.DATABASE, name: 'Database Egress', color: 'green' }, - { key: EgressType.STORAGE, name: 'Storage Egress', color: 'blue' }, - { key: EgressType.REALTIME, name: 'Realtime Egress', color: 'orange' }, - { key: EgressType.FUNCTIONS, name: 'Functions Egress', color: 'purple' }, - { key: EgressType.SUPAVISOR, name: 'Shared Pooler Egress', color: 'red' }, - { key: EgressType.LOGDRAIN, name: 'Logdrain Egress', color: 'teal' }, - ], - name: 'Total Egress', - unit: 'bytes', - description: - 'Contains any outgoing traffic including Database, Storage, Realtime, Auth, API, Edge Functions, Pooler and Log Drains.\nBilling is based on the total sum of egress in GB throughout your billing period.', - chartDescription: 'The data refreshes every 24 hours.', - }, - ], - }, - { - key: 'sizeCount', - name: 'Database & Storage Size', - description: 'Amount of resources your project is consuming', - attributes: [ - subscription?.plan.id === 'free' - ? { - anchor: 'dbSize', - key: PricingMetric.DATABASE_SIZE, - attributes: [{ key: PricingMetric.DATABASE_SIZE.toLowerCase(), color: 'white' }], - name: 'Database size', - chartPrefix: 'Average', - unit: 'bytes', - description: - 'Database size refers to the actual amount of space used by all your database objects, as reported by Postgres.', - links: [ - { - name: 'Documentation', - url: 'https://supabase.com/docs/guides/platform/database-size', - }, - ], - chartDescription: 'The data refreshes every 24 hours.', - additionalInfo: (usage?: OrgUsageResponse) => { - const usageMeta = usage?.usages.find((x) => x.metric === PricingMetric.DATABASE_SIZE) - const usageRatio = - typeof usageMeta !== 'number' - ? (usageMeta?.usage ?? 0) / (usageMeta?.pricing_free_units ?? 0) - : 0 - const hasLimit = usageMeta && (usageMeta?.pricing_free_units ?? 0) > 0 +) => { + const egressAttributes: CategoryAttribute[] = [ + { + anchor: 'egress', + key: PricingMetric.EGRESS, + attributes: [ + { key: EgressType.AUTH, name: 'Auth Egress', color: 'yellow' }, + { key: EgressType.DATABASE, name: 'Database Egress', color: 'green' }, + { key: EgressType.STORAGE, name: 'Storage Egress', color: 'blue' }, + { key: EgressType.REALTIME, name: 'Realtime Egress', color: 'orange' }, + { key: EgressType.FUNCTIONS, name: 'Functions Egress', color: 'purple' }, + { key: EgressType.SUPAVISOR, name: 'Shared Pooler Egress', color: 'red' }, + { key: EgressType.LOGDRAIN, name: 'Logdrain Egress', color: 'teal' }, + ], + name: 'Egress', + unit: 'bytes', + description: + subscription?.cached_egress_enabled === true + ? 'Contains any outgoing traffic including Database, Storage, Realtime, Auth, API, Edge Functions, Pooler and Log Drains.\nBilling is based on the total sum of uncached egress in GB throughout your billing period.\nEgress via cache hits is billed separately.' + : 'Contains any outgoing traffic including Database, Storage, Realtime, Auth, API, Edge Functions, Pooler and Log Drains.\nBilling is based on the total sum of uncached egress in GB throughout your billing period.', + chartDescription: + 'The breakdown of different egress types is inclusive of cached egress, even though it is billed separately. The data refreshes every 24 hours.', + links: [ + { + name: 'Documentation', + url: 'https://supabase.com/docs/guides/platform/manage-your-usage/egress', + }, + ], + }, + ] - const isApproachingLimit = hasLimit && usageRatio >= USAGE_APPROACHING_THRESHOLD - const isExceededLimit = hasLimit && usageRatio >= 1 - const isCapped = usageMeta?.capped + if (subscription?.cached_egress_enabled) { + egressAttributes.push({ + anchor: 'cachedEgress', + key: PricingMetric.CACHED_EGRESS, + attributes: [{ key: PricingMetric.CACHED_EGRESS.toLowerCase(), color: 'white' }], + name: 'Cached Egress', + unit: 'bytes', + description: + 'Contains any outgoing traffic that is served from a cache hit. Includes API, Storage and Edge Functions.\nBilling is based on the total sum of cached egress in GB throughout your billing period.', + chartDescription: 'The data refreshes every 24 hours.', + links: [ + { + name: 'Documentation', + url: 'https://supabase.com/docs/guides/platform/manage-your-usage/egress', + }, + ], + }) + } - const onFreePlan = subscription?.plan?.name === 'Free' + return [ + { + key: 'egress', + name: 'Egress', + description: 'Amount of data transmitted over all network connections', + attributes: egressAttributes, + }, + { + key: 'sizeCount', + name: 'Database & Storage Size', + description: 'Amount of resources your project is consuming', + attributes: [ + subscription?.plan.id === 'free' + ? { + anchor: 'dbSize', + key: PricingMetric.DATABASE_SIZE, + attributes: [{ key: PricingMetric.DATABASE_SIZE.toLowerCase(), color: 'white' }], + name: 'Database size', + chartPrefix: 'Average', + unit: 'bytes', + description: + 'Database size refers to the actual amount of space used by all your database objects, as reported by Postgres.', + links: [ + { + name: 'Documentation', + url: 'https://supabase.com/docs/guides/platform/database-size', + }, + ], + chartDescription: 'The data refreshes every 24 hours.', + additionalInfo: (usage?: OrgUsageResponse) => { + const usageMeta = usage?.usages.find( + (x) => x.metric === PricingMetric.DATABASE_SIZE + ) + const usageRatio = + typeof usageMeta !== 'number' + ? (usageMeta?.usage ?? 0) / (usageMeta?.pricing_free_units ?? 0) + : 0 + const hasLimit = usageMeta && (usageMeta?.pricing_free_units ?? 0) > 0 - return ( -
- {(isApproachingLimit || isExceededLimit) && isCapped && ( - -
-
- When you reach your database size limit, your project can go into - read-only mode.{' '} - {onFreePlan - ? 'Please upgrade your Plan.' - : "Disable your spend cap to scale seamlessly, and pay for over-usage beyond your Plan's quota."} + const isApproachingLimit = hasLimit && usageRatio >= USAGE_APPROACHING_THRESHOLD + const isExceededLimit = hasLimit && usageRatio >= 1 + const isCapped = usageMeta?.capped + + const onFreePlan = subscription?.plan?.name === 'Free' + + return ( +
+ {(isApproachingLimit || isExceededLimit) && isCapped && ( + +
+
+ When you reach your database size limit, your project can go into + read-only mode.{' '} + {onFreePlan + ? 'Please upgrade your Plan.' + : "Disable your spend cap to scale seamlessly, and pay for over-usage beyond your Plan's quota."} +
-
- - )} -
- ) + + )} +
+ ) + }, + } + : { + anchor: 'diskSize', + key: 'diskSize', + attributes: [], + name: 'Disk size', + chartPrefix: 'Average', + unit: 'bytes', + description: + "Each Supabase project comes with a dedicated disk. Each project gets 8 GB of disk for free. Billing is based on the provisioned disk size. Disk automatically scales up when you get close to it's size.\nEach hour your project is using more than 8 GB of GP3 disk, it incurs the overages in GB-Hrs, i.e. a 16 GB disk incurs 8 GB-Hrs every hour. Extra disk size costs $0.125/GB/month ($0.000171/GB-Hr).", + links: [ + { + name: 'Documentation', + url: 'https://supabase.com/docs/guides/platform/manage-your-usage/disk-size', + }, + { + name: 'Disk Management', + url: 'https://supabase.com/docs/guides/platform/database-size#disk-management', + }, + ], + chartDescription: '', }, - } - : { - anchor: 'diskSize', - key: 'diskSize', - attributes: [], - name: 'Disk size', - chartPrefix: 'Average', - unit: 'bytes', - description: - "Each Supabase project comes with a dedicated disk. Each project gets 8 GB of disk for free. Billing is based on the provisioned disk size. Disk automatically scales up when you get close to it's size.\nEach hour your project is using more than 8 GB of GP3 disk, it incurs the overages in GB-Hrs, i.e. a 16 GB disk incurs 8 GB-Hrs every hour. Extra disk size costs $0.125/GB/month ($0.000171/GB-Hr).", - links: [ - { - name: 'Documentation', - url: 'https://supabase.com/docs/guides/platform/manage-your-usage/disk-size', - }, - { - name: 'Disk Management', - url: 'https://supabase.com/docs/guides/platform/database-size#disk-management', - }, - ], - chartDescription: '', - }, - { - anchor: 'storageSize', - key: PricingMetric.STORAGE_SIZE, - attributes: [{ key: PricingMetric.STORAGE_SIZE.toLowerCase(), color: 'white' }], - name: 'Storage Size', - chartPrefix: 'Average', - unit: 'bytes', - description: - 'Sum of all objects in your storage buckets.\nBilling is prorated down to the hour and will be displayed GB-Hrs.', - chartDescription: 'The data refreshes every 24 hours.', - links: [ - { - name: 'Storage', - url: 'https://supabase.com/docs/guides/storage', - }, - ], - }, - ], - }, - { - key: 'activity', - name: 'Activity', - description: 'Usage statistics that reflect the activity of your project', - attributes: [ - { - anchor: 'mau', - key: PricingMetric.MONTHLY_ACTIVE_USERS, - attributes: [{ key: PricingMetric.MONTHLY_ACTIVE_USERS.toLowerCase(), color: 'white' }], - name: 'Monthly Active Users', - chartPrefix: 'Cumulative', - chartSuffix: 'in billing period', - unit: 'absolute', - description: - 'Users who log in or refresh their token count towards MAU.\nBilling is based on the sum of distinct users requesting your API throughout the billing period. Resets every billing cycle.', - chartDescription: - 'The data is refreshed over a period of 24 hours and resets at the beginning of every billing period.\nThe data points are relative to the beginning of your billing period and will reset with your billing period.', - links: [ - { - name: 'Auth', - url: 'https://supabase.com/docs/guides/auth', - }, - ], - }, - { - anchor: 'mauSso', - key: PricingMetric.MONTHLY_ACTIVE_SSO_USERS, - attributes: [{ key: PricingMetric.MONTHLY_ACTIVE_SSO_USERS.toLowerCase(), color: 'white' }], - name: 'Monthly Active SSO Users', - chartPrefix: 'Cumulative', - chartSuffix: 'in billing period', - unit: 'absolute', - description: - 'SSO users who log in or refresh their token count towards SSO MAU.\nBilling is based on the sum of distinct Single Sign-On users requesting your API throughout the billing period. Resets every billing cycle.', - chartDescription: - 'The data refreshes over a period of 24 hours and resets at the beginning of every billing period.\nThe data points are relative to the beginning of your billing period and will reset with your billing period.', - links: [ - { - name: 'SSO with SAML 2.0', - url: 'https://supabase.com/docs/guides/auth/sso/auth-sso-saml', - }, - ], - }, - { - anchor: 'storageImageTransformations', - key: PricingMetric.STORAGE_IMAGES_TRANSFORMED, - attributes: [ - { key: PricingMetric.STORAGE_IMAGES_TRANSFORMED.toLowerCase(), color: 'white' }, - ], - name: 'Storage Image Transformations', - chartPrefix: 'Cumulative', - chartSuffix: 'in billing period', - unit: 'absolute', - description: - 'We count all images that were transformed in the billing period, ignoring any transformations.\nUsage example: You transform one image with four different size transformations and another image with just a single transformation. It counts as two, as only two images were transformed.\nBilling is based on the count of (origin) images that used transformations throughout the billing period. Resets every billing cycle.', - chartDescription: - 'The data refreshes every 24 hours.\nThe data points are relative to the beginning of your billing period and will reset with your billing period.', - links: [ - { - name: 'Documentation', - url: 'https://supabase.com/docs/guides/storage/image-transformations', - }, - ], - }, - { - anchor: 'funcInvocations', - key: PricingMetric.FUNCTION_INVOCATIONS, - attributes: [{ key: PricingMetric.FUNCTION_INVOCATIONS.toLowerCase(), color: 'white' }], - name: 'Edge Function Invocations', - unit: 'absolute', - description: - 'Every serverless function invocation independent of response status is counted.\nBilling is based on the sum of all invocations throughout your billing period.', - chartDescription: 'The data refreshes every 24 hours.', - links: [ - { - name: 'Edge Functions', - url: 'https://supabase.com/docs/guides/functions', - }, - ], - }, - { - anchor: 'realtimeMessageCount', - key: PricingMetric.REALTIME_MESSAGE_COUNT, - attributes: [{ key: PricingMetric.REALTIME_MESSAGE_COUNT.toLowerCase(), color: 'white' }], - name: 'Realtime Messages', - unit: 'absolute', - description: - "Count of messages going through Realtime. Includes database changes, broadcast and presence. \nUsage example: If you do a database change and 5 clients listen to that change via Realtime, that's 5 messages. If you broadcast a message and 4 clients listen to that, that's 5 messages (1 message sent, 4 received).\nBilling is based on the total amount of messages throughout your billing period.", - chartDescription: 'The data refreshes every 24 hours.', - links: [ - { - name: 'Realtime Quotas', - url: 'https://supabase.com/docs/guides/realtime/quotas', - }, - ], - }, - { - anchor: 'realtimePeakConnections', - key: PricingMetric.REALTIME_PEAK_CONNECTIONS, - attributes: [ - { key: PricingMetric.REALTIME_PEAK_CONNECTIONS.toLowerCase(), color: 'white' }, - ], - name: 'Realtime Concurrent Peak Connections', - chartPrefix: 'Max', - unit: 'absolute', - description: - 'Total number of successful connections. Connections attempts are not counted towards usage.\nBilling is based on the maximum amount of concurrent peak connections throughout your billing period.', - chartDescription: 'The data refreshes every 24 hours.', - links: [ - { - name: 'Realtime Quotas', - url: 'https://supabase.com/docs/guides/realtime/quotas', - }, - ], - }, - ], - }, -] + { + anchor: 'storageSize', + key: PricingMetric.STORAGE_SIZE, + attributes: [{ key: PricingMetric.STORAGE_SIZE.toLowerCase(), color: 'white' }], + name: 'Storage Size', + chartPrefix: 'Average', + unit: 'bytes', + description: + 'Sum of all objects in your storage buckets.\nBilling is prorated down to the hour and will be displayed GB-Hrs.', + chartDescription: 'The data refreshes every 24 hours.', + links: [ + { + name: 'Storage', + url: 'https://supabase.com/docs/guides/storage', + }, + ], + }, + ], + }, + { + key: 'activity', + name: 'Activity', + description: 'Usage statistics that reflect the activity of your project', + attributes: [ + { + anchor: 'mau', + key: PricingMetric.MONTHLY_ACTIVE_USERS, + attributes: [{ key: PricingMetric.MONTHLY_ACTIVE_USERS.toLowerCase(), color: 'white' }], + name: 'Monthly Active Users', + chartPrefix: 'Cumulative', + chartSuffix: 'in billing period', + unit: 'absolute', + description: + 'Users who log in or refresh their token count towards MAU.\nBilling is based on the sum of distinct users requesting your API throughout the billing period. Resets every billing cycle.', + chartDescription: + 'The data is refreshed over a period of 24 hours and resets at the beginning of every billing period.\nThe data points are relative to the beginning of your billing period and will reset with your billing period.', + links: [ + { + name: 'Auth', + url: 'https://supabase.com/docs/guides/auth', + }, + ], + }, + { + anchor: 'mauSso', + key: PricingMetric.MONTHLY_ACTIVE_SSO_USERS, + attributes: [ + { key: PricingMetric.MONTHLY_ACTIVE_SSO_USERS.toLowerCase(), color: 'white' }, + ], + name: 'Monthly Active SSO Users', + chartPrefix: 'Cumulative', + chartSuffix: 'in billing period', + unit: 'absolute', + description: + 'SSO users who log in or refresh their token count towards SSO MAU.\nBilling is based on the sum of distinct Single Sign-On users requesting your API throughout the billing period. Resets every billing cycle.', + chartDescription: + 'The data refreshes over a period of 24 hours and resets at the beginning of every billing period.\nThe data points are relative to the beginning of your billing period and will reset with your billing period.', + links: [ + { + name: 'SSO with SAML 2.0', + url: 'https://supabase.com/docs/guides/auth/sso/auth-sso-saml', + }, + ], + }, + { + anchor: 'storageImageTransformations', + key: PricingMetric.STORAGE_IMAGES_TRANSFORMED, + attributes: [ + { key: PricingMetric.STORAGE_IMAGES_TRANSFORMED.toLowerCase(), color: 'white' }, + ], + name: 'Storage Image Transformations', + chartPrefix: 'Cumulative', + chartSuffix: 'in billing period', + unit: 'absolute', + description: + 'We count all images that were transformed in the billing period, ignoring any transformations.\nUsage example: You transform one image with four different size transformations and another image with just a single transformation. It counts as two, as only two images were transformed.\nBilling is based on the count of (origin) images that used transformations throughout the billing period. Resets every billing cycle.', + chartDescription: + 'The data refreshes every 24 hours.\nThe data points are relative to the beginning of your billing period and will reset with your billing period.', + links: [ + { + name: 'Documentation', + url: 'https://supabase.com/docs/guides/storage/image-transformations', + }, + ], + }, + { + anchor: 'funcInvocations', + key: PricingMetric.FUNCTION_INVOCATIONS, + attributes: [{ key: PricingMetric.FUNCTION_INVOCATIONS.toLowerCase(), color: 'white' }], + name: 'Edge Function Invocations', + unit: 'absolute', + description: + 'Every serverless function invocation independent of response status is counted.\nBilling is based on the sum of all invocations throughout your billing period.', + chartDescription: 'The data refreshes every 24 hours.', + links: [ + { + name: 'Edge Functions', + url: 'https://supabase.com/docs/guides/functions', + }, + ], + }, + { + anchor: 'realtimeMessageCount', + key: PricingMetric.REALTIME_MESSAGE_COUNT, + attributes: [{ key: PricingMetric.REALTIME_MESSAGE_COUNT.toLowerCase(), color: 'white' }], + name: 'Realtime Messages', + unit: 'absolute', + description: + "Count of messages going through Realtime. Includes database changes, broadcast and presence. \nUsage example: If you do a database change and 5 clients listen to that change via Realtime, that's 5 messages. If you broadcast a message and 4 clients listen to that, that's 5 messages (1 message sent, 4 received).\nBilling is based on the total amount of messages throughout your billing period.", + chartDescription: 'The data refreshes every 24 hours.', + links: [ + { + name: 'Realtime Quotas', + url: 'https://supabase.com/docs/guides/realtime/quotas', + }, + ], + }, + { + anchor: 'realtimePeakConnections', + key: PricingMetric.REALTIME_PEAK_CONNECTIONS, + attributes: [ + { key: PricingMetric.REALTIME_PEAK_CONNECTIONS.toLowerCase(), color: 'white' }, + ], + name: 'Realtime Concurrent Peak Connections', + chartPrefix: 'Max', + unit: 'absolute', + description: + 'Total number of successful connections. Connections attempts are not counted towards usage.\nBilling is based on the maximum amount of concurrent peak connections throughout your billing period.', + chartDescription: 'The data refreshes every 24 hours.', + links: [ + { + name: 'Realtime Quotas', + url: 'https://supabase.com/docs/guides/realtime/quotas', + }, + ], + }, + ], + }, + ] +} diff --git a/apps/studio/components/interfaces/Organization/Usage/Usage.tsx b/apps/studio/components/interfaces/Organization/Usage/Usage.tsx index 93796f2623e..80a68493edf 100644 --- a/apps/studio/components/interfaces/Organization/Usage/Usage.tsx +++ b/apps/studio/components/interfaces/Organization/Usage/Usage.tsx @@ -22,7 +22,7 @@ import { cn, Listbox } from 'ui' import { Admonition } from 'ui-patterns' import { Restriction } from '../BillingSettings/Restriction' import Activity from './Activity' -import Bandwidth from './Bandwidth' +import Egress from './Egress' import Compute from './Compute' import SizeAndCounts from './SizeAndCounts' import TotalUsage from './TotalUsage' @@ -234,7 +234,7 @@ const Usage = () => { /> )} - { + const onSubmit = async () => { setOpen(false) sendEvent({ action: 'realtime_inspector_listen_channel_clicked', @@ -61,8 +62,17 @@ export const ChooseChannelPopover = ({ config, onChangeConfig }: ChooseChannelPo organization: org?.slug ?? 'Unknown', }, }) + + let token = config.token + + // [Joshen] Refresh if starting to listen + using temp API key, since it has a low refresh rate + if (token.startsWith('sb_temp')) { + const data = await getTemporaryAPIKey({ projectRef: config.projectRef, expiry: 3600 }) + token = data.api_key + } onChangeConfig({ ...config, + token, channelName: form.getValues('channel'), isChannelPrivate: form.getValues('isPrivate'), enabled: true, diff --git a/apps/studio/components/interfaces/Realtime/Inspector/Header.tsx b/apps/studio/components/interfaces/Realtime/Inspector/Header.tsx index d1bdff3faf3..08f008921b1 100644 --- a/apps/studio/components/interfaces/Realtime/Inspector/Header.tsx +++ b/apps/studio/components/interfaces/Realtime/Inspector/Header.tsx @@ -1,9 +1,12 @@ +import { PermissionAction } from '@supabase/shared-types/out/constants' import { PlayCircle, StopCircle } from 'lucide-react' import { Dispatch, SetStateAction } from 'react' import { useParams } from 'common' import { ButtonTooltip } from 'components/ui/ButtonTooltip' +import { getTemporaryAPIKey } from 'data/api-keys/temp-api-keys-query' import { useSendEventMutation } from 'data/telemetry/send-event-mutation' +import { useAsyncCheckProjectPermissions } from 'hooks/misc/useCheckPermissions' import { useSelectedOrganizationQuery } from 'hooks/misc/useSelectedOrganization' import { ChooseChannelPopover } from './ChooseChannelPopover' import { RealtimeFilterPopover } from './RealtimeFilterPopover' @@ -20,6 +23,11 @@ export const Header = ({ config, onChangeConfig }: HeaderProps) => { const { ref } = useParams() const { data: org } = useSelectedOrganizationQuery() + const { can: canReadAPIKeys } = useAsyncCheckProjectPermissions( + PermissionAction.READ, + 'service_api_keys' + ) + return (
@@ -29,10 +37,18 @@ export const Header = ({ config, onChangeConfig }: HeaderProps) => { size="tiny" type={config.enabled ? 'warning' : 'primary'} className="rounded-l-none border-l-0" - disabled={config.channelName.length === 0} + disabled={!canReadAPIKeys || config.channelName.length === 0} icon={config.enabled ? : } - onClick={() => { - onChangeConfig({ ...config, enabled: !config.enabled }) + onClick={async () => { + // [Joshen] Refresh if starting to listen + using temp API key, since it has a low refresh rate + if (!config.enabled && config.token.startsWith('sb_temp')) { + const data = await getTemporaryAPIKey({ projectRef: config.projectRef, expiry: 3600 }) + const token = data.api_key + onChangeConfig({ ...config, token, enabled: !config.enabled }) + } else { + onChangeConfig({ ...config, enabled: !config.enabled }) + } + if (!config.enabled) { // the user has clicked to start listening sendEvent({ @@ -47,8 +63,11 @@ export const Header = ({ config, onChangeConfig }: HeaderProps) => { tooltip={{ content: { side: 'bottom', - text: - config.channelName.length === 0 ? 'You need to join a channel first' : undefined, + text: !canReadAPIKeys + ? 'You need additional permissions to use the realtime inspector' + : config.channelName.length === 0 + ? 'You need to join a channel first' + : undefined, }, }} > diff --git a/apps/studio/components/interfaces/Realtime/Inspector/MessagesTable.tsx b/apps/studio/components/interfaces/Realtime/Inspector/MessagesTable.tsx index 95fd72a350a..8f5a24e2d41 100644 --- a/apps/studio/components/interfaces/Realtime/Inspector/MessagesTable.tsx +++ b/apps/studio/components/interfaces/Realtime/Inspector/MessagesTable.tsx @@ -1,3 +1,4 @@ +import { PermissionAction } from '@supabase/shared-types/out/constants' import { isEqual } from 'lodash' import { ExternalLink, Loader2, Megaphone } from 'lucide-react' import Link from 'next/link' @@ -6,10 +7,13 @@ import DataGrid, { Row } from 'react-data-grid' import { useParams } from 'common' import { DocsButton } from 'components/ui/DocsButton' +import NoPermission from 'components/ui/NoPermission' import ShimmerLine from 'components/ui/ShimmerLine' import { useSendEventMutation } from 'data/telemetry/send-event-mutation' +import { useAsyncCheckProjectPermissions } from 'hooks/misc/useCheckPermissions' import { useSelectedOrganizationQuery } from 'hooks/misc/useSelectedOrganization' import { Button, IconBroadcast, IconDatabaseChanges, IconPresence, cn } from 'ui' +import { GenericSkeletonLoader } from 'ui-patterns' import MessageSelection from './MessageSelection' import type { LogData } from './Messages.types' import NoChannelEmptyState from './NoChannelEmptyState' @@ -30,9 +34,18 @@ const NoResultAlert = ({ }) => { const { ref } = useParams() + const { can: canReadAPIKeys, isLoading: isLoadingPermissions } = useAsyncCheckProjectPermissions( + PermissionAction.READ, + 'service_api_keys' + ) + return (
- {!hasChannelSet ? ( + {isLoadingPermissions ? ( + + ) : !canReadAPIKeys ? ( + + ) : !hasChannelSet ? ( ) : ( <> diff --git a/apps/studio/components/interfaces/Realtime/Inspector/RealtimeTokensPopover/index.tsx b/apps/studio/components/interfaces/Realtime/Inspector/RealtimeTokensPopover/index.tsx index ef75536f4f4..2ed1e7515e7 100644 --- a/apps/studio/components/interfaces/Realtime/Inspector/RealtimeTokensPopover/index.tsx +++ b/apps/studio/components/interfaces/Realtime/Inspector/RealtimeTokensPopover/index.tsx @@ -3,8 +3,8 @@ import { toast } from 'sonner' import { useParams } from 'common' import { RoleImpersonationPopover } from 'components/interfaces/RoleImpersonationSelector' -import { InlineLink } from 'components/ui/InlineLink' import { getKeys, useAPIKeysQuery } from 'data/api-keys/api-keys-query' +import { getTemporaryAPIKey } from 'data/api-keys/temp-api-keys-query' import { useProjectPostgrestConfigQuery } from 'data/config/project-postgrest-config-query' import { useSendEventMutation } from 'data/telemetry/send-event-mutation' import { useSelectedOrganizationQuery } from 'hooks/misc/useSelectedOrganization' @@ -27,12 +27,13 @@ export const RealtimeTokensPopover = ({ config, onChangeConfig }: RealtimeTokens projectRef: config.projectRef, reveal: true, }) - const { anonKey, serviceKey, publishableKey } = getKeys(apiKeys) + const { anonKey, publishableKey } = getKeys(apiKeys) const { data: postgrestConfig } = useProjectPostgrestConfigQuery( { projectRef: config.projectRef }, { enabled: IS_PLATFORM } ) + const jwtSecret = postgrestConfig?.jwt_secret const { mutate: sendEvent } = useSendEventMutation() @@ -62,12 +63,17 @@ export const RealtimeTokensPopover = ({ config, onChangeConfig }: RealtimeTokens snap.role !== undefined && snap.role.type === 'postgrest' ) { - token = anonKey?.api_key + token = publishableKey?.api_key ?? anonKey?.api_key await getRoleImpersonationJWT(config.projectRef, jwtSecret, snap.role) .then((b) => (bearer = b)) .catch((err) => toast.error(`Failed to get JWT for role: ${err.message}`)) } else { - token = serviceKey?.api_key ?? publishableKey?.api_key + try { + const data = await getTemporaryAPIKey({ projectRef: config.projectRef, expiry: 3600 }) + token = data.api_key + } catch (error) { + token = publishableKey?.api_key + } } if (token) { onChangeConfig({ ...config, token, bearer }) @@ -76,24 +82,7 @@ export const RealtimeTokensPopover = ({ config, onChangeConfig }: RealtimeTokens triggerUpdateTokenBearer() // eslint-disable-next-line react-hooks/exhaustive-deps - }, [snap.role, anonKey, serviceKey]) + }, [snap.role, anonKey]) - return ( - - Role impersonation for the Realtime Inspector is currently unavailable temporarily due - to the new API keys. Please re-enable{' '} - legacy JWT keys if - you'd like to use role impersonation with the Realtime Inspector. - - ) : undefined - } - align="start" - variant="connected-on-both" - /> - ) + return } diff --git a/apps/studio/components/interfaces/Realtime/Inspector/index.tsx b/apps/studio/components/interfaces/Realtime/Inspector/index.tsx index f742ea955e3..28b616b7735 100644 --- a/apps/studio/components/interfaces/Realtime/Inspector/index.tsx +++ b/apps/studio/components/interfaces/Realtime/Inspector/index.tsx @@ -14,8 +14,8 @@ import { RealtimeConfig, useRealtimeMessages } from './useRealtimeMessages' export const RealtimeInspector = () => { const { ref } = useParams() const { data: org } = useSelectedOrganizationQuery() - const [sendMessageShown, setSendMessageShown] = useState(false) + const [sendMessageShown, setSendMessageShown] = useState(false) const [realtimeConfig, setRealtimeConfig] = useState({ enabled: false, projectRef: ref!, diff --git a/apps/studio/components/interfaces/Realtime/RealtimeSettings.tsx b/apps/studio/components/interfaces/Realtime/RealtimeSettings.tsx index a9e9e2c861e..e9caa8b52e0 100644 --- a/apps/studio/components/interfaces/Realtime/RealtimeSettings.tsx +++ b/apps/studio/components/interfaces/Realtime/RealtimeSettings.tsx @@ -1,7 +1,6 @@ import { zodResolver } from '@hookform/resolvers/zod' import { PermissionAction } from '@supabase/shared-types/out/constants' import Link from 'next/link' -import { useEffect } from 'react' import { SubmitHandler, useForm } from 'react-hook-form' import { toast } from 'sonner' import * as z from 'zod' @@ -18,7 +17,7 @@ import { REALTIME_DEFAULT_CONFIG, useRealtimeConfigurationQuery, } from 'data/realtime/realtime-config-query' -import { useCheckPermissions } from 'hooks/misc/useCheckPermissions' +import { useAsyncCheckProjectPermissions } from 'hooks/misc/useCheckPermissions' import { useSelectedOrganizationQuery } from 'hooks/misc/useSelectedOrganization' import { useSelectedProjectQuery } from 'hooks/misc/useSelectedProject' import { @@ -42,17 +41,20 @@ export const RealtimeSettings = () => { const { ref: projectRef } = useParams() const { data: project } = useSelectedProjectQuery() const { data: organization } = useSelectedOrganizationQuery() - const canUpdateConfig = useCheckPermissions(PermissionAction.REALTIME_ADMIN_READ, '*') + const { can: canUpdateConfig } = useAsyncCheckProjectPermissions( + PermissionAction.REALTIME_ADMIN_READ, + '*' + ) const { data: maxConn } = useMaxConnectionsQuery({ projectRef: project?.ref, connectionString: project?.connectionString, }) - const { data, error, isLoading, isSuccess, isError } = useRealtimeConfigurationQuery({ + const { data, error, isLoading, isError } = useRealtimeConfigurationQuery({ projectRef, }) - const { data: policies } = useDatabasePoliciesQuery({ + const { data: policies, isSuccess: isSuccessPolicies } = useDatabasePoliciesQuery({ projectRef, connectionString: project?.connectionString, schema: 'realtime', @@ -96,6 +98,10 @@ export const RealtimeSettings = () => { ...REALTIME_DEFAULT_CONFIG, allow_public: !REALTIME_DEFAULT_CONFIG.private_only, }, + values: { + ...(data ?? REALTIME_DEFAULT_CONFIG), + allow_public: !(data?.private_only ?? REALTIME_DEFAULT_CONFIG.private_only), + } as any, }) const { allow_public } = form.watch() @@ -111,14 +117,6 @@ export const RealtimeSettings = () => { }) } - useEffect(() => { - // [Joshen] Temp typed with any - API typing marks all the properties as nullable, - // but checked with Filipe that they're not supposed to - if (isSuccess) { - form.reset({ ...data, allow_public: !data.private_only } as any) - } - }, [isSuccess]) - return ( @@ -136,7 +134,7 @@ export const RealtimeSettings = () => { className="!p-0 !pt-2" header={Channel restrictions} > - + { - {!hasRealtimeMessagesPolicies && !allow_public && ( + {isSuccessPolicies && !hasRealtimeMessagesPolicies && !allow_public && ( { } > - + { } > - + ) => { return (
diff --git a/apps/studio/components/interfaces/Reports/Reports.tsx b/apps/studio/components/interfaces/Reports/Reports.tsx index 1bd29985dff..f7f1f24c37e 100644 --- a/apps/studio/components/interfaces/Reports/Reports.tsx +++ b/apps/studio/components/interfaces/Reports/Reports.tsx @@ -22,7 +22,7 @@ import { useContentUpsertMutation, } from 'data/content/content-upsert-mutation' import { useSendEventMutation } from 'data/telemetry/send-event-mutation' -import { useCheckPermissions } from 'hooks/misc/useCheckPermissions' +import { useAsyncCheckProjectPermissions } from 'hooks/misc/useCheckPermissions' import { useSelectedOrganizationQuery } from 'hooks/misc/useSelectedOrganization' import { useSelectedProjectQuery } from 'hooks/misc/useSelectedProject' import { Metric, TIME_PERIODS_REPORTS } from 'lib/constants/metrics' @@ -82,22 +82,30 @@ const Reports = () => { const currentReport = userContents?.content.find((report) => report.id === id) const currentReportContent = currentReport?.content as Dashboards.Content - const canReadReport = useCheckPermissions(PermissionAction.READ, 'user_content', { - resource: { - type: 'report', - visibility: currentReport?.visibility, - owner_id: currentReport?.owner_id, - }, - subject: { id: profile?.id }, - }) - const canUpdateReport = useCheckPermissions(PermissionAction.UPDATE, 'user_content', { - resource: { - type: 'report', - visibility: currentReport?.visibility, - owner_id: currentReport?.owner_id, - }, - subject: { id: profile?.id }, - }) + const { can: canReadReport, isLoading: isLoadingPermissions } = useAsyncCheckProjectPermissions( + PermissionAction.READ, + 'user_content', + { + resource: { + type: 'report', + visibility: currentReport?.visibility, + owner_id: currentReport?.owner_id, + }, + subject: { id: profile?.id }, + } + ) + const { can: canUpdateReport } = useAsyncCheckProjectPermissions( + PermissionAction.UPDATE, + 'user_content', + { + resource: { + type: 'report', + visibility: currentReport?.visibility, + owner_id: currentReport?.owner_id, + }, + subject: { id: profile?.id }, + } + ) function handleDateRangePicker({ period_start, period_end }: any) { setStartDate(period_start.date) @@ -383,7 +391,7 @@ const Reports = () => { } }, [hasEdits, confirmNavigate, router]) - if (isLoading) { + if (isLoading || isLoadingPermissions) { return } diff --git a/apps/studio/components/interfaces/SQLEditor/SQLEditor.tsx b/apps/studio/components/interfaces/SQLEditor/SQLEditor.tsx index 9f3f8e0c174..12383978073 100644 --- a/apps/studio/components/interfaces/SQLEditor/SQLEditor.tsx +++ b/apps/studio/components/interfaces/SQLEditor/SQLEditor.tsx @@ -1,4 +1,3 @@ -import { useCompletion } from '@ai-sdk/react' import type { Monaco } from '@monaco-editor/react' import { useQueryClient } from '@tanstack/react-query' import { ChevronUp, Loader2 } from 'lucide-react' @@ -453,22 +452,70 @@ export const SQLEditor = () => { closeDiff() }, [closeDiff, resetPrompt, sendEvent]) - const { - complete, - completion, - isLoading: isCompletionLoading, - } = useCompletion({ - api: `${BASE_PATH}/api/ai/code/complete`, - body: { - projectRef: project?.ref, - connectionString: project?.connectionString, - language: 'sql', - orgSlug: org?.slug, + const [isCompletionLoading, setIsCompletionLoading] = useState(false) + + const complete = useCallback( + async ( + prompt: string, + options?: { + headers?: Record + body?: { completionMetadata?: any } + } + ) => { + try { + setIsCompletionLoading(true) + + const response = await fetch(`${BASE_PATH}/api/ai/code/complete`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + ...(options?.headers ?? {}), + }, + body: JSON.stringify({ + projectRef: project?.ref, + connectionString: project?.connectionString, + language: 'sql', + orgSlug: org?.slug, + ...(options?.body ?? {}), + }), + }) + + if (!response.ok) { + const errorText = await response.text() + throw new Error(errorText || 'Failed to generate completion') + } + + // API returns a JSON-encoded string + const text: string = await response.json() + + const meta = options?.body?.completionMetadata ?? {} + const beforeSelection: string = meta.textBeforeCursor ?? '' + const afterSelection: string = meta.textAfterCursor ?? '' + const selection: string = meta.selection ?? '' + + const original = beforeSelection + selection + afterSelection + const modified = beforeSelection + text + afterSelection + + const formattedModified = formatSql(modified) + setSourceSqlDiff({ original, modified: formattedModified }) + setSelectedDiffType(DiffType.Modification) + setPromptState((prev) => ({ ...prev, isLoading: false })) + setIsCompletionLoading(false) + } catch (error: any) { + toast.error(`Failed to generate SQL: ${error?.message ?? 'Unknown error'}`) + setIsCompletionLoading(false) + throw error + } }, - onError: (error) => { - toast.error(`Failed to generate SQL: ${error.message}`) - }, - }) + [ + org?.slug, + project?.connectionString, + project?.ref, + setPromptState, + setSelectedDiffType, + setSourceSqlDiff, + ] + ) const handlePrompt = async ( prompt: string, @@ -593,35 +640,6 @@ export const SQLEditor = () => { // eslint-disable-next-line react-hooks/exhaustive-deps }, [snapV2.diffContent]) - useEffect(() => { - if (!completion) { - return - } - - const original = - promptState.beforeSelection + promptState.selection + promptState.afterSelection - const modified = promptState.beforeSelection + completion + promptState.afterSelection - - if (isCompletionLoading) { - // Attempt to format the modified SQL in case the LLM left out indentation, etc - let formattedModified = formatSql(modified) - - setSourceSqlDiff({ - original, - modified: formattedModified, - }) - setSelectedDiffType(DiffType.Modification) - setPromptState((prev) => ({ ...prev, isLoading: false })) - } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [ - completion, - promptState.beforeSelection, - promptState.selection, - promptState.afterSelection, - isCompletionLoading, - ]) - // We want to check if the diff editor is mounted and if it is, we want to show the widget // We also want to cleanup the widget when the diff editor is closed useEffect(() => { diff --git a/apps/studio/components/interfaces/SQLEditor/SQLTemplates/SQLQuickstarts.tsx b/apps/studio/components/interfaces/SQLEditor/SQLTemplates/SQLQuickstarts.tsx index 856b9295f85..eae83009246 100644 --- a/apps/studio/components/interfaces/SQLEditor/SQLTemplates/SQLQuickstarts.tsx +++ b/apps/studio/components/interfaces/SQLEditor/SQLTemplates/SQLQuickstarts.tsx @@ -7,7 +7,7 @@ import { useParams } from 'common' import { SQL_TEMPLATES } from 'components/interfaces/SQLEditor/SQLEditor.queries' import { ActionCard } from 'components/layouts/Tabs/ActionCard' import { useSendEventMutation } from 'data/telemetry/send-event-mutation' -import { useCheckPermissions } from 'hooks/misc/useCheckPermissions' +import { useAsyncCheckProjectPermissions } from 'hooks/misc/useCheckPermissions' import { useSelectedOrganizationQuery } from 'hooks/misc/useSelectedOrganization' import { useSelectedProjectQuery } from 'hooks/misc/useSelectedProject' import { uuidv4 } from 'lib/helpers' @@ -26,10 +26,14 @@ const SQLQuickstarts = () => { const snapV2 = useSqlEditorV2StateSnapshot() - const canCreateSQLSnippet = useCheckPermissions(PermissionAction.CREATE, 'user_content', { - resource: { type: 'sql', owner_id: profile?.id }, - subject: { id: profile?.id }, - }) + const { can: canCreateSQLSnippet } = useAsyncCheckProjectPermissions( + PermissionAction.CREATE, + 'user_content', + { + resource: { type: 'sql', owner_id: profile?.id }, + subject: { id: profile?.id }, + } + ) const { mutate: sendEvent } = useSendEventMutation() diff --git a/apps/studio/components/interfaces/SQLEditor/SQLTemplates/SQLTemplates.tsx b/apps/studio/components/interfaces/SQLEditor/SQLTemplates/SQLTemplates.tsx index fdb8549bb03..40603c824f5 100644 --- a/apps/studio/components/interfaces/SQLEditor/SQLTemplates/SQLTemplates.tsx +++ b/apps/studio/components/interfaces/SQLEditor/SQLTemplates/SQLTemplates.tsx @@ -7,7 +7,7 @@ import { useParams } from 'common' import { SQL_TEMPLATES } from 'components/interfaces/SQLEditor/SQLEditor.queries' import { ActionCard } from 'components/layouts/Tabs/ActionCard' import { useSendEventMutation } from 'data/telemetry/send-event-mutation' -import { useCheckPermissions } from 'hooks/misc/useCheckPermissions' +import { useAsyncCheckProjectPermissions } from 'hooks/misc/useCheckPermissions' import { useSelectedOrganizationQuery } from 'hooks/misc/useSelectedOrganization' import { useSelectedProjectQuery } from 'hooks/misc/useSelectedProject' import { uuidv4 } from 'lib/helpers' @@ -26,10 +26,14 @@ const SQLTemplates = () => { const snapV2 = useSqlEditorV2StateSnapshot() - const canCreateSQLSnippet = useCheckPermissions(PermissionAction.CREATE, 'user_content', { - resource: { type: 'sql', owner_id: profile?.id }, - subject: { id: profile?.id }, - }) + const { can: canCreateSQLSnippet } = useAsyncCheckProjectPermissions( + PermissionAction.CREATE, + 'user_content', + { + resource: { type: 'sql', owner_id: profile?.id }, + subject: { id: profile?.id }, + } + ) const { mutate: sendEvent } = useSendEventMutation() diff --git a/apps/studio/components/interfaces/SQLEditor/hooks.ts b/apps/studio/components/interfaces/SQLEditor/hooks.ts index bd1ac73fae7..8bba5cf16b4 100644 --- a/apps/studio/components/interfaces/SQLEditor/hooks.ts +++ b/apps/studio/components/interfaces/SQLEditor/hooks.ts @@ -4,7 +4,7 @@ import { useCallback, useEffect, useMemo, useState } from 'react' import { toast } from 'sonner' import { useParams } from 'common' -import { useCheckPermissions } from 'hooks/misc/useCheckPermissions' +import { useAsyncCheckProjectPermissions } from 'hooks/misc/useCheckPermissions' import { useSelectedProjectQuery } from 'hooks/misc/useSelectedProject' import { uuidv4 } from 'lib/helpers' import { useProfile } from 'lib/profile' @@ -24,10 +24,14 @@ export const useNewQuery = () => { const { data: project } = useSelectedProjectQuery() const snapV2 = useSqlEditorV2StateSnapshot() - const canCreateSQLSnippet = useCheckPermissions(PermissionAction.CREATE, 'user_content', { - resource: { type: 'sql', owner_id: profile?.id }, - subject: { id: profile?.id }, - }) + const { can: canCreateSQLSnippet } = useAsyncCheckProjectPermissions( + PermissionAction.CREATE, + 'user_content', + { + resource: { type: 'sql', owner_id: profile?.id }, + subject: { id: profile?.id }, + } + ) const newQuery = async (sql: string, name: string, shouldRedirect: boolean = true) => { if (!ref) return console.error('Project ref is required') diff --git a/apps/studio/components/interfaces/Settings/API/PostgrestConfig.tsx b/apps/studio/components/interfaces/Settings/API/PostgrestConfig.tsx index 5285c0c7343..34f4c0085f2 100644 --- a/apps/studio/components/interfaces/Settings/API/PostgrestConfig.tsx +++ b/apps/studio/components/interfaces/Settings/API/PostgrestConfig.tsx @@ -21,7 +21,7 @@ import { useProjectPostgrestConfigQuery } from 'data/config/project-postgrest-co import { useProjectPostgrestConfigUpdateMutation } from 'data/config/project-postgrest-config-update-mutation' import { useDatabaseExtensionsQuery } from 'data/database-extensions/database-extensions-query' import { useSchemasQuery } from 'data/database/schemas-query' -import { useCheckPermissions } from 'hooks/misc/useCheckPermissions' +import { useAsyncCheckProjectPermissions } from 'hooks/misc/useCheckPermissions' import { useSelectedProjectQuery } from 'hooks/misc/useSelectedProject' import { AlertDescription_Shadcn_, @@ -101,7 +101,7 @@ export const PostgrestConfig = () => { const formId = 'project-postgres-config' const hiddenSchema = ['auth', 'pgbouncer', 'hooks', 'extensions'] - const canUpdatePostgrestConfig = useCheckPermissions( + const { can: canUpdatePostgrestConfig } = useAsyncCheckProjectPermissions( PermissionAction.UPDATE, 'custom_config_postgrest' ) diff --git a/apps/studio/components/interfaces/Settings/Addons/CustomDomainSidePanel.tsx b/apps/studio/components/interfaces/Settings/Addons/CustomDomainSidePanel.tsx index ba6ed51e6c3..26be3a22534 100644 --- a/apps/studio/components/interfaces/Settings/Addons/CustomDomainSidePanel.tsx +++ b/apps/studio/components/interfaces/Settings/Addons/CustomDomainSidePanel.tsx @@ -8,7 +8,7 @@ import { useProjectAddonRemoveMutation } from 'data/subscriptions/project-addon- import { useProjectAddonUpdateMutation } from 'data/subscriptions/project-addon-update-mutation' import { useProjectAddonsQuery } from 'data/subscriptions/project-addons-query' import type { AddonVariantId } from 'data/subscriptions/types' -import { useCheckPermissions } from 'hooks/misc/useCheckPermissions' +import { useAsyncCheckProjectPermissions } from 'hooks/misc/useCheckPermissions' import { useSelectedOrganizationQuery } from 'hooks/misc/useSelectedOrganization' import { useFlag } from 'hooks/ui/useFlag' import { formatCurrency } from 'lib/helpers' @@ -32,7 +32,7 @@ const CustomDomainSidePanel = () => { const [selectedOption, setSelectedOption] = useState('cd_none') - const canUpdateCustomDomain = useCheckPermissions( + const { can: canUpdateCustomDomain } = useAsyncCheckProjectPermissions( PermissionAction.BILLING_WRITE, 'stripe.subscriptions' ) diff --git a/apps/studio/components/interfaces/Settings/Addons/IPv4SidePanel.tsx b/apps/studio/components/interfaces/Settings/Addons/IPv4SidePanel.tsx index 3aea5b7bdc6..81d1a1dbb79 100644 --- a/apps/studio/components/interfaces/Settings/Addons/IPv4SidePanel.tsx +++ b/apps/studio/components/interfaces/Settings/Addons/IPv4SidePanel.tsx @@ -10,7 +10,7 @@ import { useProjectAddonRemoveMutation } from 'data/subscriptions/project-addon- import { useProjectAddonUpdateMutation } from 'data/subscriptions/project-addon-update-mutation' import { useProjectAddonsQuery } from 'data/subscriptions/project-addons-query' import type { AddonVariantId } from 'data/subscriptions/types' -import { useCheckPermissions } from 'hooks/misc/useCheckPermissions' +import { useAsyncCheckProjectPermissions } from 'hooks/misc/useCheckPermissions' import { useSelectedOrganizationQuery } from 'hooks/misc/useSelectedOrganization' import { useIsAwsCloudProvider } from 'hooks/misc/useSelectedProject' import { formatCurrency } from 'lib/helpers' @@ -25,7 +25,10 @@ const IPv4SidePanel = () => { const [selectedOption, setSelectedOption] = useState('ipv4_none') - const canUpdateIPv4 = useCheckPermissions(PermissionAction.BILLING_WRITE, 'stripe.subscriptions') + const { can: canUpdateIPv4 } = useAsyncCheckProjectPermissions( + PermissionAction.BILLING_WRITE, + 'stripe.subscriptions' + ) const { panel, closePanel } = useAddonsPagePanel() const visible = panel === 'ipv4' diff --git a/apps/studio/components/interfaces/Settings/Addons/PITRSidePanel.tsx b/apps/studio/components/interfaces/Settings/Addons/PITRSidePanel.tsx index b60820f43b3..8e80cf20d87 100644 --- a/apps/studio/components/interfaces/Settings/Addons/PITRSidePanel.tsx +++ b/apps/studio/components/interfaces/Settings/Addons/PITRSidePanel.tsx @@ -13,7 +13,7 @@ import { useProjectAddonRemoveMutation } from 'data/subscriptions/project-addon- import { useProjectAddonUpdateMutation } from 'data/subscriptions/project-addon-update-mutation' import { useProjectAddonsQuery } from 'data/subscriptions/project-addons-query' import type { AddonVariantId } from 'data/subscriptions/types' -import { useCheckPermissions } from 'hooks/misc/useCheckPermissions' +import { useAsyncCheckProjectPermissions } from 'hooks/misc/useCheckPermissions' import { useSelectedOrganizationQuery } from 'hooks/misc/useSelectedOrganization' import { useSelectedProjectQuery } from 'hooks/misc/useSelectedProject' import { BASE_PATH } from 'lib/constants' @@ -61,7 +61,10 @@ const PITRSidePanel = () => { const [selectedCategory, setSelectedCategory] = useState<'on' | 'off'>('off') const [selectedOption, setSelectedOption] = useState('pitr_0') - const canUpdatePitr = useCheckPermissions(PermissionAction.BILLING_WRITE, 'stripe.subscriptions') + const { can: canUpdatePitr } = useAsyncCheckProjectPermissions( + PermissionAction.BILLING_WRITE, + 'stripe.subscriptions' + ) const isBranchingEnabled = project?.is_branch_enabled === true || project?.parent_project_ref !== undefined diff --git a/apps/studio/components/interfaces/Settings/Database/BannedIPs.tsx b/apps/studio/components/interfaces/Settings/Database/BannedIPs.tsx index d38efacba8c..29ec00bb246 100644 --- a/apps/studio/components/interfaces/Settings/Database/BannedIPs.tsx +++ b/apps/studio/components/interfaces/Settings/Database/BannedIPs.tsx @@ -12,7 +12,7 @@ import { FormPanel } from 'components/ui/Forms/FormPanel' import { useBannedIPsDeleteMutation } from 'data/banned-ips/banned-ips-delete-mutations' import { useBannedIPsQuery } from 'data/banned-ips/banned-ips-query' import { useUserIPAddressQuery } from 'data/misc/user-ip-address-query' -import { useCheckPermissions } from 'hooks/misc/useCheckPermissions' +import { useAsyncCheckProjectPermissions } from 'hooks/misc/useCheckPermissions' import { useSelectedProjectQuery } from 'hooks/misc/useSelectedProject' import { Badge, Skeleton } from 'ui' import ConfirmationModal from 'ui-patterns/Dialogs/ConfirmationModal' @@ -39,11 +39,15 @@ const BannedIPs = () => { const [showUnban, setShowUnban] = useState(false) const [confirmingIP, setConfirmingIP] = useState(null) // Track the IP being confirmed for unban - const canUnbanNetworks = useCheckPermissions(PermissionAction.UPDATE, 'projects', { - resource: { - project_id: project?.id, - }, - }) + const { can: canUnbanNetworks } = useAsyncCheckProjectPermissions( + PermissionAction.UPDATE, + 'projects', + { + resource: { + project_id: project?.id, + }, + } + ) const { mutate: unbanIPs, isLoading: isUnbanning } = useBannedIPsDeleteMutation({ onSuccess: () => { diff --git a/apps/studio/components/interfaces/Settings/Database/ConnectionPooling/ConnectionPooling.tsx b/apps/studio/components/interfaces/Settings/Database/ConnectionPooling/ConnectionPooling.tsx index c993af5c00f..66127cfa7c9 100644 --- a/apps/studio/components/interfaces/Settings/Database/ConnectionPooling/ConnectionPooling.tsx +++ b/apps/studio/components/interfaces/Settings/Database/ConnectionPooling/ConnectionPooling.tsx @@ -17,7 +17,7 @@ import { useMaxConnectionsQuery } from 'data/database/max-connections-query' import { usePgbouncerConfigQuery } from 'data/database/pgbouncer-config-query' import { usePgbouncerConfigurationUpdateMutation } from 'data/database/pgbouncer-config-update-mutation' import { useProjectAddonsQuery } from 'data/subscriptions/project-addons-query' -import { useCheckPermissions } from 'hooks/misc/useCheckPermissions' +import { useAsyncCheckProjectPermissions } from 'hooks/misc/useCheckPermissions' import { useSelectedOrganizationQuery } from 'hooks/misc/useSelectedOrganization' import { useSelectedProjectQuery } from 'hooks/misc/useSelectedProject' import { @@ -51,7 +51,7 @@ export const ConnectionPooling = () => { const { data: project } = useSelectedProjectQuery() const { data: org } = useSelectedOrganizationQuery() - const canUpdateConnectionPoolingConfiguration = useCheckPermissions( + const { can: canUpdateConnectionPoolingConfiguration } = useAsyncCheckProjectPermissions( PermissionAction.UPDATE, 'projects', { resource: { project_id: project?.id } } diff --git a/apps/studio/components/interfaces/Settings/Database/DatabaseSettings/ResetDbPassword.tsx b/apps/studio/components/interfaces/Settings/Database/DatabaseSettings/ResetDbPassword.tsx index f3e4787a425..b39ac8008fa 100644 --- a/apps/studio/components/interfaces/Settings/Database/DatabaseSettings/ResetDbPassword.tsx +++ b/apps/studio/components/interfaces/Settings/Database/DatabaseSettings/ResetDbPassword.tsx @@ -9,7 +9,7 @@ import { ButtonTooltip } from 'components/ui/ButtonTooltip' import Panel from 'components/ui/Panel' import PasswordStrengthBar from 'components/ui/PasswordStrengthBar' import { useDatabasePasswordResetMutation } from 'data/database/database-password-reset-mutation' -import { useCheckPermissions } from 'hooks/misc/useCheckPermissions' +import { useAsyncCheckProjectPermissions } from 'hooks/misc/useCheckPermissions' import { useSelectedProjectQuery } from 'hooks/misc/useSelectedProject' import { DEFAULT_MINIMUM_PASSWORD_STRENGTH } from 'lib/constants' import passwordStrength from 'lib/password-strength' @@ -20,11 +20,16 @@ const ResetDbPassword = ({ disabled = false }) => { const { ref } = useParams() const isProjectActive = useIsProjectActive() const { data: project } = useSelectedProjectQuery() - const canResetDbPassword = useCheckPermissions(PermissionAction.UPDATE, 'projects', { - resource: { - project_id: project?.id, - }, - }) + + const { can: canResetDbPassword } = useAsyncCheckProjectPermissions( + PermissionAction.UPDATE, + 'projects', + { + resource: { + project_id: project?.id, + }, + } + ) const [showResetDbPass, setShowResetDbPass] = useState(false) diff --git a/apps/studio/components/interfaces/Settings/Database/DiskSizeConfiguration.tsx b/apps/studio/components/interfaces/Settings/Database/DiskSizeConfiguration.tsx index da6544a74b3..f8ae07220fe 100644 --- a/apps/studio/components/interfaces/Settings/Database/DiskSizeConfiguration.tsx +++ b/apps/studio/components/interfaces/Settings/Database/DiskSizeConfiguration.tsx @@ -12,7 +12,7 @@ import { FormHeader } from 'components/ui/Forms/FormHeader' import Panel from 'components/ui/Panel' import { useProjectDiskResizeMutation } from 'data/config/project-disk-resize-mutation' import { useDatabaseSizeQuery } from 'data/database/database-size-query' -import { useCheckPermissions } from 'hooks/misc/useCheckPermissions' +import { useAsyncCheckProjectPermissions } from 'hooks/misc/useCheckPermissions' import { useSelectedOrganizationQuery } from 'hooks/misc/useSelectedOrganization' import { useSelectedProjectQuery } from 'hooks/misc/useSelectedProject' import { useUrlState } from 'hooks/ui/useUrlState' @@ -35,11 +35,15 @@ const DiskSizeConfiguration = ({ disabled = false }: DiskSizeConfigurationProps) setUrlParams({ show_increase_disk_size_modal: show ? 'true' : undefined }) } - const canUpdateDiskSizeConfig = useCheckPermissions(PermissionAction.UPDATE, 'projects', { - resource: { - project_id: project?.id, - }, - }) + const { can: canUpdateDiskSizeConfig } = useAsyncCheckProjectPermissions( + PermissionAction.UPDATE, + 'projects', + { + resource: { + project_id: project?.id, + }, + } + ) const { isLoading: isUpdatingDiskSize } = useProjectDiskResizeMutation({ onSuccess: (_, variables) => { diff --git a/apps/studio/components/interfaces/Settings/Database/NetworkRestrictions/NetworkRestrictions.tsx b/apps/studio/components/interfaces/Settings/Database/NetworkRestrictions/NetworkRestrictions.tsx index c03e52b884f..4418578341e 100644 --- a/apps/studio/components/interfaces/Settings/Database/NetworkRestrictions/NetworkRestrictions.tsx +++ b/apps/studio/components/interfaces/Settings/Database/NetworkRestrictions/NetworkRestrictions.tsx @@ -10,7 +10,7 @@ import { FormPanel } from 'components/ui/Forms/FormPanel' import Panel from 'components/ui/Panel' import ShimmeringLoader from 'components/ui/ShimmeringLoader' import { useNetworkRestrictionsQuery } from 'data/network-restrictions/network-restrictions-query' -import { useCheckPermissions } from 'hooks/misc/useCheckPermissions' +import { useAsyncCheckProjectPermissions } from 'hooks/misc/useCheckPermissions' import { useSelectedProjectQuery } from 'hooks/misc/useSelectedProject' import { Badge, @@ -75,11 +75,15 @@ const NetworkRestrictions = () => { const [selectedRestrictionToRemove, setSelectedRestrictionToRemove] = useState() const { data, isLoading } = useNetworkRestrictionsQuery({ projectRef: ref }) - const canUpdateNetworkRestrictions = useCheckPermissions(PermissionAction.UPDATE, 'projects', { - resource: { - project_id: project?.id, - }, - }) + const { can: canUpdateNetworkRestrictions } = useAsyncCheckProjectPermissions( + PermissionAction.UPDATE, + 'projects', + { + resource: { + project_id: project?.id, + }, + } + ) const hasAccessToRestrictions = data?.entitlement === 'allowed' const ipv4Restrictions = data?.config?.dbAllowedCidrs ?? [] diff --git a/apps/studio/components/interfaces/Settings/Database/SSLConfiguration.tsx b/apps/studio/components/interfaces/Settings/Database/SSLConfiguration.tsx index 1b5448d4ed9..52990b97e1e 100644 --- a/apps/studio/components/interfaces/Settings/Database/SSLConfiguration.tsx +++ b/apps/studio/components/interfaces/Settings/Database/SSLConfiguration.tsx @@ -13,7 +13,7 @@ import { FormSection, FormSectionContent, FormSectionLabel } from 'components/ui import { useProjectSettingsV2Query } from 'data/config/project-settings-v2-query' import { useSSLEnforcementQuery } from 'data/ssl-enforcement/ssl-enforcement-query' import { useSSLEnforcementUpdateMutation } from 'data/ssl-enforcement/ssl-enforcement-update-mutation' -import { useCheckPermissions } from 'hooks/misc/useCheckPermissions' +import { useAsyncCheckProjectPermissions } from 'hooks/misc/useCheckPermissions' import { useSelectedProjectQuery } from 'hooks/misc/useSelectedProject' import { Alert, Button, Switch, Tooltip, TooltipContent, TooltipTrigger } from 'ui' @@ -42,11 +42,15 @@ const SSLConfiguration = () => { } ) - const canUpdateSSLEnforcement = useCheckPermissions(PermissionAction.UPDATE, 'projects', { - resource: { - project_id: project?.id, - }, - }) + const { can: canUpdateSSLEnforcement } = useAsyncCheckProjectPermissions( + PermissionAction.UPDATE, + 'projects', + { + resource: { + project_id: project?.id, + }, + } + ) const initialIsEnforced = isSuccess ? sslEnforcementConfiguration.appliedSuccessfully && sslEnforcementConfiguration.currentConfig.database @@ -143,7 +147,7 @@ const SSLConfiguration = () => { ? 'You need additional permissions to update SSL enforcement for your project' : !hasAccessToSSLEnforcement ? 'Your project does not have access to SSL enforcement' - : ''} + : undefined} )} diff --git a/apps/studio/components/interfaces/Settings/General/ComplianceConfig/ProjectComplianceMode.tsx b/apps/studio/components/interfaces/Settings/General/ComplianceConfig/ProjectComplianceMode.tsx index 05e0cbc1020..0f33a4c6e12 100644 --- a/apps/studio/components/interfaces/Settings/General/ComplianceConfig/ProjectComplianceMode.tsx +++ b/apps/studio/components/interfaces/Settings/General/ComplianceConfig/ProjectComplianceMode.tsx @@ -12,7 +12,7 @@ import { FormSection, FormSectionContent, FormSectionLabel } from 'components/ui import { InlineLink } from 'components/ui/InlineLink' import { useComplianceConfigUpdateMutation } from 'data/config/project-compliance-config-mutation' import { useProjectSettingsV2Query } from 'data/config/project-settings-v2-query' -import { useCheckPermissions } from 'hooks/misc/useCheckPermissions' +import { useAsyncCheckProjectPermissions } from 'hooks/misc/useCheckPermissions' import { useSelectedProjectQuery } from 'hooks/misc/useSelectedProject' import { Switch, Tooltip, TooltipContent, TooltipTrigger } from 'ui' @@ -21,9 +21,13 @@ const ComplianceConfig = () => { const { data: project } = useSelectedProjectQuery() const [isSensitive, setIsSensitive] = useState(false) - const canUpdateComplianceConfig = useCheckPermissions(PermissionAction.UPDATE, 'projects', { - resource: { project_id: project?.id }, - }) + const { can: canUpdateComplianceConfig } = useAsyncCheckProjectPermissions( + PermissionAction.UPDATE, + 'projects', + { + resource: { project_id: project?.id }, + } + ) const { data: settings, diff --git a/apps/studio/components/interfaces/Settings/General/CustomDomainConfig/CustomDomainsConfigureHostname.tsx b/apps/studio/components/interfaces/Settings/General/CustomDomainConfig/CustomDomainsConfigureHostname.tsx index e0ab0c151b2..1f7f77233c6 100644 --- a/apps/studio/components/interfaces/Settings/General/CustomDomainConfig/CustomDomainsConfigureHostname.tsx +++ b/apps/studio/components/interfaces/Settings/General/CustomDomainConfig/CustomDomainsConfigureHostname.tsx @@ -9,7 +9,7 @@ import { FormSection, FormSectionContent, FormSectionLabel } from 'components/ui import { useProjectSettingsV2Query } from 'data/config/project-settings-v2-query' import { useCheckCNAMERecordMutation } from 'data/custom-domains/check-cname-mutation' import { useCustomDomainCreateMutation } from 'data/custom-domains/custom-domains-create-mutation' -import { useCheckPermissions } from 'hooks/misc/useCheckPermissions' +import { useAsyncCheckProjectPermissions } from 'hooks/misc/useCheckPermissions' import { useSelectedProjectQuery } from 'hooks/misc/useSelectedProject' import { Form, Input } from 'ui' @@ -27,11 +27,15 @@ const CustomDomainsConfigureHostname = () => { const FORM_ID = 'custom-domains-form' const endpoint = settings?.app_config?.endpoint - const canConfigureCustomDomain = useCheckPermissions(PermissionAction.UPDATE, 'projects', { - resource: { - project_id: project?.id, - }, - }) + const { can: canConfigureCustomDomain } = useAsyncCheckProjectPermissions( + PermissionAction.UPDATE, + 'projects', + { + resource: { + project_id: project?.id, + }, + } + ) const onCreateCustomDomain = async (values: yup.InferType) => { if (!ref) return console.error('Project ref is required') diff --git a/apps/studio/components/interfaces/Settings/General/DeleteProjectPanel/DeleteProjectButton.tsx b/apps/studio/components/interfaces/Settings/General/DeleteProjectPanel/DeleteProjectButton.tsx index c86e9f68d86..a47ecbb6cd6 100644 --- a/apps/studio/components/interfaces/Settings/General/DeleteProjectPanel/DeleteProjectButton.tsx +++ b/apps/studio/components/interfaces/Settings/General/DeleteProjectPanel/DeleteProjectButton.tsx @@ -2,7 +2,7 @@ import { PermissionAction } from '@supabase/shared-types/out/constants' import { useState } from 'react' import { ButtonTooltip } from 'components/ui/ButtonTooltip' -import { useCheckPermissions } from 'hooks/misc/useCheckPermissions' +import { useAsyncCheckProjectPermissions } from 'hooks/misc/useCheckPermissions' import { useSelectedProjectQuery } from 'hooks/misc/useSelectedProject' import { DeleteProjectModal } from './DeleteProjectModal' @@ -14,9 +14,13 @@ const DeleteProjectButton = ({ type = 'danger' }: DeleteProjectButtonProps) => { const { data: project } = useSelectedProjectQuery() const [isOpen, setIsOpen] = useState(false) - const canDeleteProject = useCheckPermissions(PermissionAction.UPDATE, 'projects', { - resource: { project_id: project?.id }, - }) + const { can: canDeleteProject } = useAsyncCheckProjectPermissions( + PermissionAction.UPDATE, + 'projects', + { + resource: { project_id: project?.id }, + } + ) return ( <> diff --git a/apps/studio/components/interfaces/Settings/General/General.tsx b/apps/studio/components/interfaces/Settings/General/General.tsx index a459c6f3515..ea95391fcaf 100644 --- a/apps/studio/components/interfaces/Settings/General/General.tsx +++ b/apps/studio/components/interfaces/Settings/General/General.tsx @@ -9,7 +9,7 @@ import { FormSection, FormSectionContent, FormSectionLabel } from 'components/ui import Panel from 'components/ui/Panel' import { GenericSkeletonLoader } from 'components/ui/ShimmeringLoader' import { useProjectUpdateMutation } from 'data/projects/project-update-mutation' -import { useCheckPermissions } from 'hooks/misc/useCheckPermissions' +import { useAsyncCheckProjectPermissions } from 'hooks/misc/useCheckPermissions' import { useSelectedOrganizationQuery } from 'hooks/misc/useSelectedOrganization' import { useProjectByRefQuery, useSelectedProjectQuery } from 'hooks/misc/useSelectedProject' import { @@ -33,11 +33,15 @@ const General = () => { const formId = 'project-general-settings' const initialValues = { name: project?.name ?? '', ref: project?.ref ?? '' } - const canUpdateProject = useCheckPermissions(PermissionAction.UPDATE, 'projects', { - resource: { - project_id: project?.id, - }, - }) + const { can: canUpdateProject } = useAsyncCheckProjectPermissions( + PermissionAction.UPDATE, + 'projects', + { + resource: { + project_id: project?.id, + }, + } + ) const { mutate: updateProject, isLoading: isUpdating } = useProjectUpdateMutation() diff --git a/apps/studio/components/interfaces/Settings/General/Infrastructure/PauseProjectButton.tsx b/apps/studio/components/interfaces/Settings/General/Infrastructure/PauseProjectButton.tsx index 9121f8caf89..1cd34e163ec 100644 --- a/apps/studio/components/interfaces/Settings/General/Infrastructure/PauseProjectButton.tsx +++ b/apps/studio/components/interfaces/Settings/General/Infrastructure/PauseProjectButton.tsx @@ -9,7 +9,7 @@ import { useIsProjectActive } from 'components/layouts/ProjectLayout/ProjectCont import { ButtonTooltip } from 'components/ui/ButtonTooltip' import { useProjectPauseMutation } from 'data/projects/project-pause-mutation' import { setProjectStatus } from 'data/projects/projects-query' -import { useCheckPermissions } from 'hooks/misc/useCheckPermissions' +import { useAsyncCheckProjectPermissions } from 'hooks/misc/useCheckPermissions' import { useSelectedOrganizationQuery } from 'hooks/misc/useSelectedOrganization' import { useIsAwsK8sCloudProvider, useSelectedProjectQuery } from 'hooks/misc/useSelectedProject' import { PROJECT_STATUS } from 'lib/constants' @@ -25,7 +25,7 @@ const PauseProjectButton = () => { const projectRef = project?.ref ?? '' const isPaused = project?.status === PROJECT_STATUS.INACTIVE - const canPauseProject = useCheckPermissions( + const { can: canPauseProject } = useAsyncCheckProjectPermissions( PermissionAction.INFRA_EXECUTE, 'queue_jobs.projects.pause' ) diff --git a/apps/studio/components/interfaces/Settings/General/Infrastructure/RestartServerButton.tsx b/apps/studio/components/interfaces/Settings/General/Infrastructure/RestartServerButton.tsx index 53777787380..55be165b81b 100644 --- a/apps/studio/components/interfaces/Settings/General/Infrastructure/RestartServerButton.tsx +++ b/apps/studio/components/interfaces/Settings/General/Infrastructure/RestartServerButton.tsx @@ -10,7 +10,7 @@ import { ButtonTooltip } from 'components/ui/ButtonTooltip' import { useProjectRestartMutation } from 'data/projects/project-restart-mutation' import { useProjectRestartServicesMutation } from 'data/projects/project-restart-services-mutation' import { setProjectStatus } from 'data/projects/projects-query' -import { useCheckPermissions } from 'hooks/misc/useCheckPermissions' +import { useAsyncCheckProjectPermissions } from 'hooks/misc/useCheckPermissions' import { useIsAwsK8sCloudProvider, useSelectedProjectQuery } from 'hooks/misc/useSelectedProject' import { useFlag } from 'hooks/ui/useFlag' import { @@ -35,7 +35,10 @@ const RestartServerButton = () => { const projectRegion = project?.region ?? '' const projectRestartDisabled = useFlag('disableProjectRestarts') - const canRestartProject = useCheckPermissions(PermissionAction.INFRA_EXECUTE, 'reboot') + const { can: canRestartProject } = useAsyncCheckProjectPermissions( + PermissionAction.INFRA_EXECUTE, + 'reboot' + ) const { mutate: restartProject, isLoading: isRestartingProject } = useProjectRestartMutation({ onSuccess: () => { diff --git a/apps/studio/components/interfaces/Settings/General/TransferProjectPanel/TransferProjectButton.tsx b/apps/studio/components/interfaces/Settings/General/TransferProjectPanel/TransferProjectButton.tsx index cad1c7cfb4e..932ad84012e 100644 --- a/apps/studio/components/interfaces/Settings/General/TransferProjectPanel/TransferProjectButton.tsx +++ b/apps/studio/components/interfaces/Settings/General/TransferProjectPanel/TransferProjectButton.tsx @@ -8,7 +8,7 @@ import { DocsButton } from 'components/ui/DocsButton' import { useOrganizationsQuery } from 'data/organizations/organizations-query' import { useProjectTransferMutation } from 'data/projects/project-transfer-mutation' import { useProjectTransferPreviewQuery } from 'data/projects/project-transfer-preview-query' -import { useCheckPermissions } from 'hooks/misc/useCheckPermissions' +import { useAsyncCheckProjectPermissions } from 'hooks/misc/useCheckPermissions' import { useSelectedProjectQuery } from 'hooks/misc/useSelectedProject' import { useFlag } from 'hooks/ui/useFlag' import { Button, InfoIcon, Listbox, Loading, Modal, WarningIcon } from 'ui' @@ -58,7 +58,10 @@ const TransferProjectButton = () => { } }, [isOpen]) - const canTransferProject = useCheckPermissions(PermissionAction.UPDATE, 'organizations') + const { can: canTransferProject } = useAsyncCheckProjectPermissions( + PermissionAction.UPDATE, + 'organizations' + ) const toggle = () => { setIsOpen(!isOpen) diff --git a/apps/studio/components/interfaces/Settings/Infrastructure/InfrastructureConfiguration/InstanceConfiguration.tsx b/apps/studio/components/interfaces/Settings/Infrastructure/InfrastructureConfiguration/InstanceConfiguration.tsx index e77f7ad6819..ea3cee853fc 100644 --- a/apps/studio/components/interfaces/Settings/Infrastructure/InfrastructureConfiguration/InstanceConfiguration.tsx +++ b/apps/studio/components/interfaces/Settings/Infrastructure/InfrastructureConfiguration/InstanceConfiguration.tsx @@ -15,7 +15,7 @@ import { ReplicaInitializationStatus, useReadReplicasStatusesQuery, } from 'data/read-replicas/replicas-status-query' -import { useCheckPermissions } from 'hooks/misc/useCheckPermissions' +import { useAsyncCheckProjectPermissions } from 'hooks/misc/useCheckPermissions' import { useIsOrioleDb, useSelectedProjectQuery } from 'hooks/misc/useSelectedProject' import { timeout } from 'lib/helpers' import Link from 'next/link' @@ -56,7 +56,10 @@ const InstanceConfigurationUI = () => { const [selectedReplicaToDrop, setSelectedReplicaToDrop] = useState() const [selectedReplicaToRestart, setSelectedReplicaToRestart] = useState() - const canManageReplicas = useCheckPermissions(PermissionAction.CREATE, 'projects') + const { can: canManageReplicas } = useAsyncCheckProjectPermissions( + PermissionAction.CREATE, + 'projects' + ) const { data: loadBalancers, diff --git a/apps/studio/components/interfaces/Settings/Infrastructure/InfrastructureConfiguration/InstanceNode.tsx b/apps/studio/components/interfaces/Settings/Infrastructure/InfrastructureConfiguration/InstanceNode.tsx index 8db1a1bb8ec..ef365b0604b 100644 --- a/apps/studio/components/interfaces/Settings/Infrastructure/InfrastructureConfiguration/InstanceNode.tsx +++ b/apps/studio/components/interfaces/Settings/Infrastructure/InfrastructureConfiguration/InstanceNode.tsx @@ -6,6 +6,7 @@ import { parseAsBoolean, useQueryState } from 'nuqs' import { Handle, NodeProps, Position } from 'reactflow' import { useParams } from 'common' +import { DropdownMenuItemTooltip } from 'components/ui/DropdownMenuItemTooltip' import SparkBar from 'components/ui/SparkBar' import { DatabaseInitEstimations, @@ -13,7 +14,7 @@ import { useReadReplicasStatusesQuery, } from 'data/read-replicas/replicas-status-query' import { formatDatabaseID } from 'data/read-replicas/replicas.utils' -import { useCheckPermissions } from 'hooks/misc/useCheckPermissions' +import { useAsyncCheckProjectPermissions } from 'hooks/misc/useCheckPermissions' import { BASE_PATH } from 'lib/constants' import { useDatabaseSelectorStateSnapshot } from 'state/database-selector' import { @@ -181,7 +182,10 @@ export const ReplicaNode = ({ data }: NodeProps) => { } = data const { ref } = useParams() const dbSelectorState = useDatabaseSelectorStateSnapshot() - const canManageReplicas = useCheckPermissions(PermissionAction.CREATE, 'projects') + const { can: canManageReplicas } = useAsyncCheckProjectPermissions( + PermissionAction.CREATE, + 'projects' + ) const [, setShowConnect] = useQueryState('showConnect', parseAsBoolean.withDefault(false)) const { data: databaseStatuses } = useReadReplicasStatusesQuery({ projectRef: ref }) @@ -370,24 +374,18 @@ export const ReplicaNode = ({ data }: NodeProps) => { {/* onSelectResizeReplica()}> Resize replica */} - - - { - if (canManageReplicas) onSelectDropReplica() - }} - > - Drop replica - - - {!canManageReplicas && ( - - You need additional permissions to drop replicas - - )} - + { + if (canManageReplicas) onSelectDropReplica() + }} + tooltip={{ + content: { side: 'left', text: 'You need additional permissions to drop replicas' }, + }} + > + Drop replica +
diff --git a/apps/studio/components/interfaces/Settings/Infrastructure/InfrastructureConfiguration/MapView.tsx b/apps/studio/components/interfaces/Settings/Infrastructure/InfrastructureConfiguration/MapView.tsx index fbcac6a6bfc..d8486c29f07 100644 --- a/apps/studio/components/interfaces/Settings/Infrastructure/InfrastructureConfiguration/MapView.tsx +++ b/apps/studio/components/interfaces/Settings/Infrastructure/InfrastructureConfiguration/MapView.tsx @@ -16,9 +16,10 @@ import { import { useParams } from 'common' import { ButtonTooltip } from 'components/ui/ButtonTooltip' +import { DropdownMenuItemTooltip } from 'components/ui/DropdownMenuItemTooltip' import { Database, useReadReplicasQuery } from 'data/read-replicas/replicas-query' import { formatDatabaseID } from 'data/read-replicas/replicas.utils' -import { useCheckPermissions } from 'hooks/misc/useCheckPermissions' +import { useAsyncCheckProjectPermissions } from 'hooks/misc/useCheckPermissions' import { BASE_PATH } from 'lib/constants' import type { AWS_REGIONS_KEYS } from 'shared-data' import { useDatabaseSelectorStateSnapshot } from 'state/database-selector' @@ -31,9 +32,6 @@ import { DropdownMenuSeparator, DropdownMenuTrigger, ScrollArea, - Tooltip, - TooltipContent, - TooltipTrigger, } from 'ui' import { AVAILABLE_REPLICA_REGIONS, REPLICA_STATUS } from './InstanceConfiguration.constants' import GeographyData from './MapData.json' @@ -62,7 +60,10 @@ const MapView = ({ y: number region: { key: string; country?: string; name?: string } }>() - const canManageReplicas = useCheckPermissions(PermissionAction.CREATE, 'projects') + const { can: canManageReplicas } = useAsyncCheckProjectPermissions( + PermissionAction.CREATE, + 'projects' + ) const [, setShowConnect] = useQueryState('showConnect', parseAsBoolean.withDefault(false)) const { data } = useReadReplicasQuery({ projectRef: ref }) @@ -325,22 +326,20 @@ const MapView = ({ > Restart replica - - - onSelectDropReplica(database)} - > - Drop replica - - - {!canManageReplicas && ( - - You need additional permissions to drop replicas - - )} - + + onSelectDropReplica(database)} + tooltip={{ + content: { + side: 'left', + text: 'You need additional permissions to drop replicas', + }, + }} + > + Drop replica + )} diff --git a/apps/studio/components/interfaces/Settings/Integrations/GithubIntegration/GitHubIntegrationConnectionForm.tsx b/apps/studio/components/interfaces/Settings/Integrations/GithubIntegration/GitHubIntegrationConnectionForm.tsx index 75540458b2e..9d7255035e5 100644 --- a/apps/studio/components/interfaces/Settings/Integrations/GithubIntegration/GitHubIntegrationConnectionForm.tsx +++ b/apps/studio/components/interfaces/Settings/Integrations/GithubIntegration/GitHubIntegrationConnectionForm.tsx @@ -16,7 +16,7 @@ import { useGitHubConnectionDeleteMutation } from 'data/integrations/github-conn import { useGitHubConnectionUpdateMutation } from 'data/integrations/github-connection-update-mutation' import { useGitHubRepositoriesQuery } from 'data/integrations/github-repositories-query' import type { GitHubConnection } from 'data/integrations/integrations.types' -import { useCheckPermissions } from 'hooks/misc/useCheckPermissions' +import { useAsyncCheckProjectPermissions } from 'hooks/misc/useCheckPermissions' import { useSelectedOrganizationQuery } from 'hooks/misc/useSelectedOrganization' import { useSelectedProjectQuery } from 'hooks/misc/useSelectedProject' import { openInstallGitHubIntegrationWindow } from 'lib/github' @@ -72,17 +72,16 @@ const GitHubIntegrationConnectionForm = ({ const [repoComboBoxOpen, setRepoComboboxOpen] = useState(false) const isParentProject = !Boolean(selectedProject?.parent_project_ref) - const canUpdateGitHubConnection = useCheckPermissions( + const { can: canUpdateGitHubConnection } = useAsyncCheckProjectPermissions( PermissionAction.UPDATE, 'integrations.github_connections' ) - const canCreateGitHubConnection = useCheckPermissions( + const { can: canCreateGitHubConnection } = useAsyncCheckProjectPermissions( PermissionAction.CREATE, 'integrations.github_connections' ) - const { data: gitHubAuthorization, isLoading: isLoadingGitHubAuthorization } = - useGitHubAuthorizationQuery() + const { data: gitHubAuthorization } = useGitHubAuthorizationQuery() const { data: githubReposData, isLoading: isLoadingGitHubRepos } = useGitHubRepositoriesQuery< any[] diff --git a/apps/studio/components/interfaces/Settings/Integrations/GithubIntegration/GithubSection.tsx b/apps/studio/components/interfaces/Settings/Integrations/GithubIntegration/GithubSection.tsx index 7e17d3cf777..36256f769ef 100644 --- a/apps/studio/components/interfaces/Settings/Integrations/GithubIntegration/GithubSection.tsx +++ b/apps/studio/components/interfaces/Settings/Integrations/GithubIntegration/GithubSection.tsx @@ -11,10 +11,11 @@ import { import NoPermission from 'components/ui/NoPermission' import UpgradeToPro from 'components/ui/UpgradeToPro' import { useGitHubConnectionsQuery } from 'data/integrations/github-connections-query' -import { useCheckPermissions } from 'hooks/misc/useCheckPermissions' +import { useAsyncCheckProjectPermissions } from 'hooks/misc/useCheckPermissions' import { useSelectedOrganizationQuery } from 'hooks/misc/useSelectedOrganization' import { BASE_PATH, IS_PLATFORM } from 'lib/constants' import { cn } from 'ui' +import { GenericSkeletonLoader } from 'ui-patterns' import GitHubIntegrationConnectionForm from './GitHubIntegrationConnectionForm' const IntegrationImageHandler = ({ title }: { title: 'vercel' | 'github' }) => { @@ -31,10 +32,8 @@ const GitHubSection = () => { const { ref: projectRef } = useParams() const { data: organization } = useSelectedOrganizationQuery() - const canReadGitHubConnection = useCheckPermissions( - PermissionAction.READ, - 'integrations.github_connections' - ) + const { can: canReadGitHubConnection, isLoading: isLoadingPermissions } = + useAsyncCheckProjectPermissions(PermissionAction.READ, 'integrations.github_connections') const isProPlanAndUp = organization?.plan?.id !== 'free' const promptProPlanUpgrade = IS_PLATFORM && !isProPlanAndUp @@ -51,22 +50,6 @@ const GitHubSection = () => { const GitHubTitle = `GitHub Integration` - if (!canReadGitHubConnection) { - return ( - - - -

Connect any of your GitHub repositories to a project.

- -
- - - -
-
- ) - } - return ( @@ -75,28 +58,34 @@ const GitHubSection = () => { -
-
-
How does the GitHub integration work?
-

- Connecting to GitHub allows you to sync preview branches with a chosen GitHub - branch, keep your production branch in sync, and automatically create preview - branches for every pull request. -

- {promptProPlanUpgrade && ( -
- + {isLoadingPermissions ? ( + + ) : !canReadGitHubConnection ? ( + + ) : ( +
+
+
How does the GitHub integration work?
+

+ Connecting to GitHub allows you to sync preview branches with a chosen GitHub + branch, keep your production branch in sync, and automatically create preview + branches for every pull request. +

+ {promptProPlanUpgrade && ( +
+ +
+ )} +
+
- )} -
-
-
+ )} diff --git a/apps/studio/components/interfaces/Settings/Integrations/VercelIntegration/VercelSection.tsx b/apps/studio/components/interfaces/Settings/Integrations/VercelIntegration/VercelSection.tsx index 0e39d99946c..78d4d7d0ff0 100644 --- a/apps/studio/components/interfaces/Settings/Integrations/VercelIntegration/VercelSection.tsx +++ b/apps/studio/components/interfaces/Settings/Integrations/VercelIntegration/VercelSection.tsx @@ -25,13 +25,14 @@ import type { IntegrationName, IntegrationProjectConnection, } from 'data/integrations/integrations.types' -import { useCheckPermissions } from 'hooks/misc/useCheckPermissions' +import { useAsyncCheckProjectPermissions } from 'hooks/misc/useCheckPermissions' import { useSelectedOrganizationQuery } from 'hooks/misc/useSelectedOrganization' import { useSelectedProjectQuery } from 'hooks/misc/useSelectedProject' import { pluralize } from 'lib/helpers' import { getIntegrationConfigurationUrl } from 'lib/integration-utils' import { useSidePanelsStateSnapshot } from 'state/side-panels' import { Button, cn } from 'ui' +import { GenericSkeletonLoader } from 'ui-patterns' import { IntegrationImageHandler } from '../IntegrationsSettings' import VercelIntegrationConnectionForm from './VercelIntegrationConnectionForm' @@ -42,15 +43,13 @@ const VercelSection = ({ isProjectScoped }: { isProjectScoped: boolean }) => { const sidePanelsStateSnapshot = useSidePanelsStateSnapshot() const isBranch = project?.parent_project_ref !== undefined - const canReadVercelConnection = useCheckPermissions( - PermissionAction.READ, - 'integrations.vercel_connections' - ) - const canCreateVercelConnection = useCheckPermissions( + const { can: canReadVercelConnection, isLoading: isLoadingPermissions } = + useAsyncCheckProjectPermissions(PermissionAction.READ, 'integrations.vercel_connections') + const { can: canCreateVercelConnection } = useAsyncCheckProjectPermissions( PermissionAction.CREATE, 'integrations.vercel_connections' ) - const canUpdateVercelConnection = useCheckPermissions( + const { can: canUpdateVercelConnection } = useAsyncCheckProjectPermissions( PermissionAction.UPDATE, 'integrations.vercel_connections' ) @@ -162,7 +161,9 @@ You can change the scope of the access for Supabase by configuring - {!canReadVercelConnection ? ( + {isLoadingPermissions ? ( + + ) : !canReadVercelConnection ? ( ) : ( <> diff --git a/apps/studio/components/interfaces/Settings/Logs/LogTable.tsx b/apps/studio/components/interfaces/Settings/Logs/LogTable.tsx index bb5e4015af7..b6e32e5d1c3 100644 --- a/apps/studio/components/interfaces/Settings/Logs/LogTable.tsx +++ b/apps/studio/components/interfaces/Settings/Logs/LogTable.tsx @@ -10,8 +10,9 @@ import { IS_PLATFORM, useParams } from 'common' import { ButtonTooltip } from 'components/ui/ButtonTooltip' import { DownloadResultsButton } from 'components/ui/DownloadResultsButton' import { useSelectedLog } from 'hooks/analytics/useSelectedLog' -import { useCheckPermissions } from 'hooks/misc/useCheckPermissions' +import { useAsyncCheckProjectPermissions } from 'hooks/misc/useCheckPermissions' import { useProfile } from 'lib/profile' +import { toast } from 'sonner' import { ResponseError } from 'types' import { Button, @@ -33,7 +34,6 @@ import { isDefaultLogPreviewFormat } from './Logs.utils' import { DefaultErrorRenderer } from './LogsErrorRenderers/DefaultErrorRenderer' import ResourcesExceededErrorRenderer from './LogsErrorRenderers/ResourcesExceededErrorRenderer' import { LogsTableEmptyState } from './LogsTableEmptyState' -import { toast } from 'sonner' interface Props { data?: LogData[] @@ -94,10 +94,14 @@ const LogTable = ({ const [selectionOpen, setSelectionOpen] = useState(false) const [selectedRow, setSelectedRow] = useState(null) - const canCreateLogQuery = useCheckPermissions(PermissionAction.CREATE, 'user_content', { - resource: { type: 'log_sql', owner_id: profile?.id }, - subject: { id: profile?.id }, - }) + const { can: canCreateLogQuery } = useAsyncCheckProjectPermissions( + PermissionAction.CREATE, + 'user_content', + { + resource: { type: 'log_sql', owner_id: profile?.id }, + subject: { id: profile?.id }, + } + ) const firstRow = data[0] diff --git a/apps/studio/components/interfaces/Settings/Logs/Logs.constants.ts b/apps/studio/components/interfaces/Settings/Logs/Logs.constants.ts index 9044fd26bd9..807688e4e83 100644 --- a/apps/studio/components/interfaces/Settings/Logs/Logs.constants.ts +++ b/apps/studio/components/interfaces/Settings/Logs/Logs.constants.ts @@ -229,21 +229,22 @@ limit 100 description: 'Check the number of requests done on Storage Affecting Egress', mode: 'custom', searchString: `select - r.method as http_verb, - r.path as filepath, - count(*) as num_requests, - from edge_logs - cross join unnest(metadata) as m - cross join unnest(m.request) AS r - cross join unnest(r.headers) AS h - where - (path like '%storage/v1/object/%' or path like '%storage/v1/render/%') - and r.method = 'GET' - group by - r.path, r.method - order by - num_requests desc - limit 100 + request.method as http_verb, + request.path as filepath, + (responseHeaders.cf_cache_status = 'HIT') as cached, + count(*) as num_requests +from + edge_logs + cross join unnest(metadata) as metadata + cross join unnest(metadata.request) as request + cross join unnest(metadata.response) as response + cross join unnest(response.headers) as responseHeaders +where + (path like '%storage/v1/object/%' or path like '%storage/v1/render/%') + and request.method = 'GET' +group by 1, 2, 3 +order by num_requests desc +limit 100; `, for: ['api'], }, @@ -265,7 +266,7 @@ where starts_with(r.path, '/storage/v1/object') and h.cf_cache_status in ('MISS', 'NONE/UNKNOWN', 'EXPIRED', 'BYPASS', 'DYNAMIC') group by path, search order by count desc -limit 100 +limit 100; `, for: ['api'], }, diff --git a/apps/studio/components/interfaces/Sidebar.tsx b/apps/studio/components/interfaces/Sidebar.tsx index 0727be8436c..2f6c7ca7c01 100644 --- a/apps/studio/components/interfaces/Sidebar.tsx +++ b/apps/studio/components/interfaces/Sidebar.tsx @@ -41,7 +41,10 @@ import { Sidebar as SidebarPrimitive, useSidebar, } from 'ui' -import { useIsAPIDocsSidePanelEnabled } from './App/FeaturePreview/FeaturePreviewContext' +import { + useIsAPIDocsSidePanelEnabled, + useUnifiedLogsPreview, +} from './App/FeaturePreview/FeaturePreviewContext' export const ICON_SIZE = 32 export const ICON_STROKE_WIDTH = 1.5 @@ -243,7 +246,12 @@ const ProjectLinks = () => { storage: storageEnabled, realtime: realtimeEnabled, }) - const otherRoutes = generateOtherRoutes(ref, project) + + const { isEnabled: isUnifiedLogsEnabled } = useUnifiedLogsPreview() + + const otherRoutes = generateOtherRoutes(ref, project, { + unifiedLogs: isUnifiedLogsEnabled, + }) const settingsRoutes = generateSettingsRoutes(ref, project) return ( diff --git a/apps/studio/components/interfaces/Storage/BucketRow.tsx b/apps/studio/components/interfaces/Storage/BucketRow.tsx index 78ef08e09c0..4023d5ab356 100644 --- a/apps/studio/components/interfaces/Storage/BucketRow.tsx +++ b/apps/studio/components/interfaces/Storage/BucketRow.tsx @@ -1,10 +1,13 @@ +import { useState } from 'react' import { PermissionAction } from '@supabase/shared-types/out/constants' -import { noop } from 'lodash' import { Columns3, Edit2, MoreVertical, Trash, XCircle } from 'lucide-react' import Link from 'next/link' import type { Bucket } from 'data/storage/buckets-query' -import { useCheckPermissions } from 'hooks/misc/useCheckPermissions' +import { useAsyncCheckProjectPermissions } from 'hooks/misc/useCheckPermissions' +import EditBucketModal from 'components/interfaces/Storage/EditBucketModal' +import DeleteBucketModal from 'components/interfaces/Storage/DeleteBucketModal' +import EmptyBucketModal from 'components/interfaces/Storage/EmptyBucketModal' import { Badge, Button, @@ -23,20 +26,15 @@ export interface BucketRowProps { bucket: Bucket projectRef?: string isSelected: boolean - onSelectEmptyBucket: () => void - onSelectDeleteBucket: () => void - onSelectEditBucket: () => void } -const BucketRow = ({ - bucket, - projectRef = '', - isSelected = false, - onSelectEmptyBucket = noop, - onSelectDeleteBucket = noop, - onSelectEditBucket = noop, -}: BucketRowProps) => { - const canUpdateBuckets = useCheckPermissions(PermissionAction.STORAGE_WRITE, '*') +const BucketRow = ({ bucket, projectRef = '', isSelected = false }: BucketRowProps) => { + const { can: canUpdateBuckets } = useAsyncCheckProjectPermissions( + PermissionAction.STORAGE_WRITE, + '*' + ) + const [modal, setModal] = useState(null) + const onClose = () => setModal(null) return (
onSelectEditBucket()} + onClick={() => setModal(`edit`)} >

Edit bucket

@@ -93,7 +91,7 @@ const BucketRow = ({ onSelectEmptyBucket()} + onClick={() => setModal(`empty`)} >

Empty bucket

@@ -103,7 +101,7 @@ const BucketRow = ({ onSelectDeleteBucket()} + onClick={() => setModal(`delete`)} >

Delete bucket

@@ -113,6 +111,10 @@ const BucketRow = ({ ) : (
)} + + + +
) } diff --git a/apps/studio/components/interfaces/Storage/CreateBucketModal.tsx b/apps/studio/components/interfaces/Storage/CreateBucketModal.tsx index 172d6a75754..db5181a618d 100644 --- a/apps/studio/components/interfaces/Storage/CreateBucketModal.tsx +++ b/apps/studio/components/interfaces/Storage/CreateBucketModal.tsx @@ -1,9 +1,9 @@ import { zodResolver } from '@hookform/resolvers/zod' import { snakeCase } from 'lodash' -import { ChevronDown } from 'lucide-react' +import { ChevronDown, Edit } from 'lucide-react' import Link from 'next/link' import { useRouter } from 'next/router' -import { useEffect, useState } from 'react' +import { useState } from 'react' import { SubmitHandler, useForm } from 'react-hook-form' import { toast } from 'sonner' import z from 'zod' @@ -24,59 +24,91 @@ import { AlertTitle_Shadcn_, Button, cn, - Collapsible, + Collapsible_Shadcn_, + CollapsibleContent_Shadcn_, + CollapsibleTrigger_Shadcn_, + Dialog, + DialogContent, + DialogFooter, + DialogHeader, + DialogSection, + DialogSectionSeparator, + DialogTitle, + DialogTrigger, Form_Shadcn_, FormControl_Shadcn_, FormField_Shadcn_, Input_Shadcn_, Label_Shadcn_, - Listbox, - Modal, RadioGroupStacked, RadioGroupStackedItem, - Toggle, + Select_Shadcn_, + SelectContent_Shadcn_, + SelectItem_Shadcn_, + SelectTrigger_Shadcn_, + SelectValue_Shadcn_, + Switch, WarningIcon, } from 'ui' import { Admonition } from 'ui-patterns/admonition' import { FormItemLayout } from 'ui-patterns/form/FormItemLayout/FormItemLayout' +import { inverseValidBucketNameRegex, validBucketNameRegex } from './CreateBucketModal.utils' +import { ButtonTooltip } from 'components/ui/ButtonTooltip' +import { PermissionAction } from '@supabase/shared-types/out/constants' import { convertFromBytes, convertToBytes } from './StorageSettings/StorageSettings.utils' +import { useAsyncCheckProjectPermissions } from 'hooks/misc/useCheckPermissions' -export interface CreateBucketModalProps { - visible: boolean - onClose: () => void -} +const FormSchema = z + .object({ + name: z + .string() + .trim() + .min(1, 'Please provide a name for your bucket') + .max(100, 'Bucket name should be below 100 characters') + .refine( + (value) => !value.endsWith(' '), + 'The name of the bucket cannot end with a whitespace' + ) + .refine( + (value) => value !== 'public', + '"public" is a reserved name. Please choose another name' + ), + type: z.enum(['STANDARD', 'ANALYTICS']).default('STANDARD'), + public: z.boolean().default(false), + has_file_size_limit: z.boolean().default(false), + formatted_size_limit: z.coerce + .number() + .min(0, 'File size upload limit has to be at least 0') + .default(0), + allowed_mime_types: z.string().trim().default(''), + }) + .superRefine((data, ctx) => { + if (!validBucketNameRegex.test(data.name)) { + const [match] = data.name.match(inverseValidBucketNameRegex) ?? [] + ctx.addIssue({ + path: ['name'], + code: z.ZodIssueCode.custom, + message: !!match + ? `Bucket name cannot contain the "${match}" character` + : 'Bucket name contains an invalid special character', + }) + } + }) -const FormSchema = z.object({ - name: z - .string() - .trim() - .min(1, 'Please provide a name for your bucket') - .regex( - /^[a-z0-9.-]+$/, - 'The name of the bucket must only contain lowercase letters, numbers, dots, and hyphens' - ) - .refine((value) => !value.endsWith(' '), 'The name of the bucket cannot end with a whitespace') - .refine( - (value) => value !== 'public', - '"public" is a reserved name. Please choose another name' - ), - type: z.enum(['STANDARD', 'ANALYTICS']).default('STANDARD'), - public: z.boolean().default(false), - has_file_size_limit: z.boolean().default(false), - formatted_size_limit: z.coerce - .number() - .min(0, 'File size upload limit has to be at least 0') - .default(0), - allowed_mime_types: z.string().trim().default(''), -}) +const formId = 'create-storage-bucket-form' export type CreateBucketForm = z.infer -const CreateBucketModal = ({ visible, onClose }: CreateBucketModalProps) => { +const CreateBucketModal = () => { + const [visible, setVisible] = useState(false) const { ref } = useParams() - const router = useRouter() const { data: org } = useSelectedOrganizationQuery() const { mutate: sendEvent } = useSendEventMutation() + const router = useRouter() + const { can: canCreateBuckets } = useAsyncCheckProjectPermissions( + PermissionAction.STORAGE_WRITE, + '*' + ) const { mutateAsync: createBucket, isLoading: isCreating } = useBucketCreateMutation({ // [Joshen] Silencing the error here as it's being handled in onSubmit @@ -89,7 +121,7 @@ const CreateBucketModal = ({ visible, onClose }: CreateBucketModalProps) => { const { value, unit } = convertFromBytes(data?.fileSizeLimit ?? 0) const formattedGlobalUploadLimit = `${value} ${unit}` - const [selectedUnit, setSelectedUnit] = useState(StorageSizeUnits.BYTES) + const [selectedUnit, setSelectedUnit] = useState(StorageSizeUnits.BYTES) const [showConfiguration, setShowConfiguration] = useState(false) const form = useForm({ @@ -110,6 +142,7 @@ const CreateBucketModal = ({ visible, onClose }: CreateBucketModalProps) => { const hasFileSizeLimit = form.watch('has_file_size_limit') const formattedSizeLimit = form.watch('formatted_size_limit') const icebergWrapperExtensionState = useIcebergWrapperExtension() + const icebergCatalogEnabled = data?.features?.icebergCatalog?.enabled const onSubmit: SubmitHandler = async (values) => { if (!ref) return console.error('Project ref is required') @@ -123,7 +156,7 @@ const CreateBucketModal = ({ visible, onClose }: CreateBucketModalProps) => { try { const fileSizeLimit = values.has_file_size_limit - ? convertToBytes(values.formatted_size_limit, selectedUnit) + ? convertToBytes(values.formatted_size_limit, selectedUnit as StorageSizeUnits) : undefined const allowedMimeTypes = @@ -148,106 +181,129 @@ const CreateBucketModal = ({ visible, onClose }: CreateBucketModalProps) => { if (values.type === 'ANALYTICS' && icebergWrapperExtensionState === 'installed') { await createIcebergWrapper({ bucketName: values.name }) } - toast.success(`Successfully created bucket ${values.name}`) - router.push(`/project/${ref}/storage/buckets/${values.name}`) - onClose() - } catch (error: any) { - toast.error(`Failed to create bucket: ${error.message}`) - } - } - - useEffect(() => { - if (visible) { form.reset() setSelectedUnit(StorageSizeUnits.BYTES) setShowConfiguration(false) + setVisible(false) + toast.success(`Successfully created bucket ${values.name}`) + router.push(`/project/${ref}/storage/buckets/${values.name}`) + } catch (error) { + console.error(error) + toast.error('Failed to create bucket') } - }, [visible, form]) + } - const icebergCatalogEnabled = data?.features?.icebergCatalog?.enabled + const handleClose = () => { + form.reset() + setSelectedUnit(StorageSizeUnits.BYTES) + setShowConfiguration(false) + setVisible(false) + } return ( - onClose()} + { + if (!open) { + handleClose() + } + }} > - -
- - ( - - - - - - )} - /> - -
+ + } + disabled={!canCreateBuckets} + style={{ justifyContent: 'start' }} + onClick={() => setVisible(true)} + tooltip={{ + content: { + side: 'bottom', + text: !canCreateBuckets + ? 'You need additional permissions to create buckets' + : undefined, + }, + }} + > + New bucket + + + + + Create storage bucket + + + + + ( - + + + + + + )} + /> + + ( + field.onChange(v)} value={field.value} + onValueChange={(v) => field.onChange(v)} > -
-
-

- Compatible with S3 buckets. -

-
-
-
+ /> {IS_PLATFORM && ( -
-
-

- Stores Iceberg files and is optimized for analytical workloads. -

-
-
- {!icebergCatalogEnabled && ( -
- - - This is currently in alpha and not enabled for your project. Sign - up{' '} - - here - - . - -
- )} + <> +

+ Stores Iceberg files and is optimized for analytical workloads. +

+ + {icebergCatalogEnabled ? null : ( +
+ + + This feature is currently in alpha and not yet enabled for your + project. Sign up{' '} + + here + + . + +
+ )} +
)}
@@ -255,26 +311,28 @@ const CreateBucketModal = ({ visible, onClose }: CreateBucketModalProps) => {
)} /> -
-
- - {isStandardBucket ? ( - <> - -
+ + + + {isStandardBucket ? ( + <> ( - + - @@ -283,235 +341,240 @@ const CreateBucketModal = ({ visible, onClose }: CreateBucketModalProps) => { {isPublicBucket && ( -

- Users can read objects in public buckets without any authorization. -

-

- Row level security (RLS) policies are still required for other operations - such as object uploads and deletes. -

-
- )} -
-
- - setShowConfiguration(!showConfiguration)} - > - -
-

Additional restrictions

- +

+ Users can read objects in public buckets without any authorization. +

+

+ Row level security (RLS) policies are still required for other + operations such as object uploads and deletes. +

+ + } /> -
-
- -
-
+ )} + setShowConfiguration(!showConfiguration)} + > + + + + +
+ ( + + + + + + )} + /> + {hasFileSizeLimit && ( +
+
+ ( + + + + + + )} + /> +
+ + + + <>{selectedUnit} + + + + {Object.values(StorageSizeUnits).map((unit: string) => ( + +
{unit}
+
+ ))} +
+
+ {IS_PLATFORM && ( +
+

+ Note: Individual bucket uploads will still be capped at the{' '} + + global upload limit + {' '} + of {formattedGlobalUploadLimit} +

+
+ )} +
+ )} +
( - + - )} /> - {hasFileSizeLimit && ( -
-
- ( - - - { - if (event.charCode < 48 || event.charCode > 57) { - event.preventDefault() - } - }} - /> - - - Equivalent to{' '} - {convertToBytes( - formattedSizeLimit, - selectedUnit - ).toLocaleString()}{' '} - bytes. - - - )} - /> -
-
- - {Object.values(StorageSizeUnits).map((unit: string) => ( - -
{unit}
-
- ))} -
-
- {IS_PLATFORM && ( -
-

- Note: Individual bucket uploads will still be capped at the{' '} - - global upload limit - {' '} - of {formattedGlobalUploadLimit} -

-
- )} -
- )} -
- ( - - - - - - )} - /> -
-
-
- - ) : ( - - {icebergWrapperExtensionState === 'installed' ? ( - -

- Supabase will setup a - - foreign data wrapper - {bucketName && {`${bucketName}_fdw`}} - - - {' '} - for easier access to the data. This action will also create{' '} - - S3 Access Keys - {bucketName && ( - <> - {' '} - named {`${bucketName}_keys`} - - )} - - and - - four Vault Secrets - {bucketName && ( - <> - {' '} - prefixed with{' '} - {`${bucketName}_vault_`} - - )} - - . - -

-

- As a final step, you'll need to create an{' '} - Iceberg namespace before you - connect the Iceberg data to your database. -

-
+ + + ) : ( - - - - You need to install the Iceberg wrapper extension to connect your Analytic - bucket to your database. - - -

- You need to install the wrappers extension - (with the minimum version of 0.5.3) if you want to connect your - Analytics bucket to your database. -

-
-
+ <> + {icebergWrapperExtensionState === 'installed' ? ( + +

+ Supabase will setup a + + foreign data wrapper + {bucketName && {`${bucketName}_fdw`}} + + + {' '} + for easier access to the data. This action will also create{' '} + + S3 Access Keys + {bucketName && ( + <> + {' '} + named {`${bucketName}_keys`} + + )} + + and + + four Vault Secrets + {bucketName && ( + <> + {' '} + prefixed with{' '} + {`${bucketName}_vault_`} + + )} + + . + +

+

+ As a final step, you'll need to create an{' '} + Iceberg namespace before you + connect the Iceberg data to your database. +

+
+ ) : ( + + + + You need to install the Iceberg wrapper extension to connect your Analytic + bucket to your database. + + +

+ You need to install the wrappers{' '} + extension (with the minimum version of 0.5.3) if you want to + connect your Analytics bucket to your database. +

+
+
+ )} + )} -
- )} - - - - - - -
-
+ + + + + + + + + ) } diff --git a/apps/studio/components/interfaces/Storage/CreateBucketModal.utils.ts b/apps/studio/components/interfaces/Storage/CreateBucketModal.utils.ts new file mode 100644 index 00000000000..2f10e8ced95 --- /dev/null +++ b/apps/studio/components/interfaces/Storage/CreateBucketModal.utils.ts @@ -0,0 +1,15 @@ +// [Joshen] These are referenced from the storage API directly here +// https://github.com/supabase/storage/blob/69e4a40799a9d10be0a63a37cbb46d7d9bea0e17/src/storage/limits.ts#L59 + +// only allow s3 safe characters and characters which require special handling for now +// the slash restriction come from bucket naming rules +// and the rest of the validation rules are based on S3 object key validation. +// https://docs.aws.amazon.com/AmazonS3/latest/userguide/object-keys.html +// https://docs.aws.amazon.com/AmazonS3/latest/userguide/bucketnamingrules.html +export const validBucketNameRegex = /^(\w|!|-|\.|\*|'|\(|\)| |&|\$|@|=|;|:|\+|,|\?)*$/ +export const inverseValidBucketNameRegex = /[^A-Za-z0-9_!\-.*'() &$@=;:+,?]/ + +// only allow s3 safe characters and characters which require special handling for now +// https://docs.aws.amazon.com/AmazonS3/latest/userguide/object-keys.html +export const validObjectKeyRegex = /^(\w|\/|!|-|\.|\*|'|\(|\)| |&|\$|@|=|;|:|\+|,|\?)*$/ +export const inverseValidObjectKeyRegex = /[^A-Za-z0-9_\/!\-.*'() &$@=;:+,?]/ diff --git a/apps/studio/components/interfaces/Storage/DeleteBucketModal.tsx b/apps/studio/components/interfaces/Storage/DeleteBucketModal.tsx index 0515641d074..509b1969d46 100644 --- a/apps/studio/components/interfaces/Storage/DeleteBucketModal.tsx +++ b/apps/studio/components/interfaces/Storage/DeleteBucketModal.tsx @@ -1,27 +1,58 @@ -import { useParams } from 'common' import { get as _get, find } from 'lodash' import { useRouter } from 'next/router' +import { SubmitHandler, useForm } from 'react-hook-form' +import { zodResolver } from '@hookform/resolvers/zod' +import z from 'zod' import { toast } from 'sonner' +import { useParams } from 'common' import { useDatabasePoliciesQuery } from 'data/database-policies/database-policies-query' import { useDatabasePolicyDeleteMutation } from 'data/database-policies/database-policy-delete-mutation' import { useBucketDeleteMutation } from 'data/storage/bucket-delete-mutation' import { Bucket, useBucketsQuery } from 'data/storage/buckets-query' import { useSelectedProjectQuery } from 'hooks/misc/useSelectedProject' -import TextConfirmModal from 'ui-patterns/Dialogs/TextConfirmModal' import { formatPoliciesForStorage } from './Storage.utils' +import { + Button, + Dialog, + DialogContent, + DialogFooter, + DialogHeader, + DialogSection, + DialogSectionSeparator, + DialogTitle, + Form_Shadcn_, + FormControl_Shadcn_, + FormField_Shadcn_, + Input_Shadcn_, + Label_Shadcn_, +} from 'ui' +import { Admonition } from 'ui-patterns' +import { FormItemLayout } from 'ui-patterns/form/FormItemLayout/FormItemLayout' export interface DeleteBucketModalProps { visible: boolean - bucket?: Bucket + bucket: Bucket onClose: () => void } -const DeleteBucketModal = ({ visible = false, bucket, onClose }: DeleteBucketModalProps) => { +const formId = `delete-storage-bucket-form` + +export const DeleteBucketModal = ({ visible, bucket, onClose }: DeleteBucketModalProps) => { const router = useRouter() const { ref: projectRef } = useParams() const { data: project } = useSelectedProjectQuery() + const schema = z.object({ + confirm: z.literal(bucket.name, { + errorMap: () => ({ message: `Please enter "${bucket.name}" to confirm` }), + }), + }) + + const form = useForm>({ + resolver: zodResolver(schema), + }) + const { data } = useBucketsQuery({ projectRef }) const { data: policies } = useDatabasePoliciesQuery({ projectRef: project?.ref, @@ -30,7 +61,7 @@ const DeleteBucketModal = ({ visible = false, bucket, onClose }: DeleteBucketMod }) const { mutateAsync: deletePolicy } = useDatabasePolicyDeleteMutation() - const { mutate: deleteBucket, isLoading: isDeleting } = useBucketDeleteMutation({ + const { mutate: deleteBucket, isLoading } = useBucketDeleteMutation({ onSuccess: async () => { if (!project) return console.error('Project is required') @@ -41,7 +72,7 @@ const DeleteBucketModal = ({ visible = false, bucket, onClose }: DeleteBucketMod storageObjectsPolicies ) const bucketPolicies = _get( - find(formattedStorageObjectPolicies, { name: bucket!.name }), + find(formattedStorageObjectPolicies, { name: bucket.name }), ['policies'], [] ) @@ -57,12 +88,12 @@ const DeleteBucketModal = ({ visible = false, bucket, onClose }: DeleteBucketMod ) ) - toast.success(`Successfully deleted bucket ${bucket?.name}`) + toast.success(`Successfully deleted bucket ${bucket.name}`) router.push(`/project/${projectRef}/storage/buckets`) onClose() } catch (error) { toast.success( - `Successfully deleted bucket ${bucket?.name}. However, there was a problem deleting the policies tied to the bucket. Please review them in the storage policies section` + `Successfully deleted bucket ${bucket.name}. However, there was a problem deleting the policies tied to the bucket. Please review them in the storage policies section` ) } }, @@ -70,34 +101,83 @@ const DeleteBucketModal = ({ visible = false, bucket, onClose }: DeleteBucketMod const buckets = data ?? [] - const onDeleteBucket = async () => { + const onSubmit: SubmitHandler> = async () => { if (!projectRef) return console.error('Project ref is required') if (!bucket) return console.error('No bucket is selected') deleteBucket({ projectRef, id: bucket.id, type: bucket.type }) } return ( - - Your bucket {bucket?.name} and all its - contents will be permanently deleted. - - } - alert={{ - title: 'You cannot recover this bucket once deleted.', - description: 'All bucket data will be lost.', + { + if (!open) { + onClose() + } }} - confirmLabel="Delete bucket" - /> + > + + + {`Confirm deletion of ${bucket.name}`} + + + + +

+ Your bucket {bucket.name} and all its + contents will be permanently deleted. +

+
+ + + +
+ ( + + Type {bucket.name} to + confirm. + + } + > + + + + + )} + /> + +
+
+ + + + +
+
) } diff --git a/apps/studio/components/interfaces/Storage/EditBucketModal.tsx b/apps/studio/components/interfaces/Storage/EditBucketModal.tsx index ba9cb5b9a51..975f340279b 100644 --- a/apps/studio/components/interfaces/Storage/EditBucketModal.tsx +++ b/apps/studio/components/interfaces/Storage/EditBucketModal.tsx @@ -1,9 +1,35 @@ import { useParams } from 'common' import { ChevronDown } from 'lucide-react' import Link from 'next/link' -import { useEffect, useState } from 'react' +import { useState } from 'react' import { toast } from 'sonner' -import { Button, Collapsible, Form, Input, Listbox, Modal, Toggle, cn } from 'ui' +import { type SubmitHandler, useForm } from 'react-hook-form' +import { zodResolver } from '@hookform/resolvers/zod' +import { z } from 'zod' +import { + Button, + CollapsibleContent_Shadcn_, + CollapsibleTrigger_Shadcn_, + Collapsible_Shadcn_, + Dialog, + DialogContent, + DialogFooter, + DialogHeader, + DialogSectionSeparator, + DialogSection, + DialogTitle, + FormControl_Shadcn_, + FormField_Shadcn_, + Form_Shadcn_, + Input_Shadcn_, + SelectContent_Shadcn_, + SelectItem_Shadcn_, + SelectTrigger_Shadcn_, + SelectValue_Shadcn_, + Select_Shadcn_, + Switch, + cn, +} from 'ui' import { StorageSizeUnits } from 'components/interfaces/Storage/StorageSettings/StorageSettings.constants' import { @@ -16,256 +42,333 @@ import { useBucketUpdateMutation } from 'data/storage/bucket-update-mutation' import { IS_PLATFORM } from 'lib/constants' import { Admonition } from 'ui-patterns' import { Bucket } from 'data/storage/buckets-query' +import { FormItemLayout } from 'ui-patterns/form/FormItemLayout/FormItemLayout' +import { isNonNullable } from 'lib/isNonNullable' export interface EditBucketModalProps { visible: boolean - bucket?: Bucket + bucket: Bucket onClose: () => void } -const EditBucketModal = ({ visible, bucket, onClose }: EditBucketModalProps) => { +const BucketSchema = z.object({ + name: z.string(), + public: z.boolean().default(false), + has_file_size_limit: z.boolean().default(false), + formatted_size_limit: z.coerce + .number() + .min(0, 'File size upload limit has to be at least 0') + .default(0), + allowed_mime_types: z.string().trim().default(''), +}) + +const formId = 'edit-storage-bucket-form' + +export const EditBucketModal = ({ visible, bucket, onClose }: EditBucketModalProps) => { const { ref } = useParams() - const { mutate: updateBucket, isLoading: isUpdating } = useBucketUpdateMutation({ - onSuccess: () => { - toast.success(`Successfully updated bucket "${bucket?.name}"`) - onClose() - }, - }) - const { data } = useProjectStorageConfigQuery( - { projectRef: ref }, - { enabled: IS_PLATFORM && visible } - ) + const { mutate: updateBucket, isLoading: isUpdating } = useBucketUpdateMutation() + const { data } = useProjectStorageConfigQuery({ projectRef: ref }, { enabled: IS_PLATFORM }) const { value, unit } = convertFromBytes(data?.fileSizeLimit ?? 0) const formattedGlobalUploadLimit = `${value} ${unit}` - const [selectedUnit, setSelectedUnit] = useState(StorageSizeUnits.BYTES) + const [selectedUnit, setSelectedUnit] = useState(StorageSizeUnits.BYTES) const [showConfiguration, setShowConfiguration] = useState(false) + const { value: fileSizeLimit } = convertFromBytes(bucket?.file_size_limit ?? 0) - const validate = (values: any) => { - const errors = {} as any - if (values.has_file_size_limit && values.formatted_size_limit < 0) { - errors.formatted_size_limit = 'File size upload limit has to be at least 0' - } - return errors - } + const form = useForm>({ + resolver: zodResolver(BucketSchema), + defaultValues: { + name: bucket?.name ?? '', + public: bucket?.public, + has_file_size_limit: isNonNullable(bucket?.file_size_limit), + formatted_size_limit: fileSizeLimit ?? 0, + allowed_mime_types: (bucket?.allowed_mime_types ?? []).join(', '), + }, + values: { + name: bucket?.name ?? '', + public: bucket?.public, + has_file_size_limit: isNonNullable(bucket?.file_size_limit), + formatted_size_limit: fileSizeLimit ?? 0, + allowed_mime_types: (bucket?.allowed_mime_types ?? []).join(', '), + }, + mode: 'onSubmit', + }) - const onSubmit = async (values: any) => { + const isPublicBucket = form.watch('public') + const hasFileSizeLimit = form.watch('has_file_size_limit') + const formattedSizeLimit = form.watch('formatted_size_limit') + const isChangingBucketVisibility = bucket?.public !== isPublicBucket + const isMakingBucketPrivate = bucket?.public && !isPublicBucket + const isMakingBucketPublic = !bucket?.public && isPublicBucket + + const onSubmit: SubmitHandler> = async (values) => { if (bucket === undefined) return console.error('Bucket is required') if (ref === undefined) return console.error('Project ref is required') - updateBucket({ - projectRef: ref, - id: bucket.id, - isPublic: values.public, - file_size_limit: values.has_file_size_limit - ? convertToBytes(values.formatted_size_limit, selectedUnit) - : null, - allowed_mime_types: - values.allowed_mime_types.length > 0 - ? values.allowed_mime_types.split(',').map((x: string) => x.trim()) + updateBucket( + { + projectRef: ref, + id: bucket.id, + isPublic: values.public, + file_size_limit: values.has_file_size_limit + ? convertToBytes(values.formatted_size_limit, selectedUnit as StorageSizeUnits) : null, - }) + allowed_mime_types: + values.allowed_mime_types.length > 0 + ? values.allowed_mime_types.split(',').map((x: string) => x.trim()) + : null, + }, + { + onSuccess: () => { + toast.success(`Successfully updated bucket "${bucket?.name}"`) + onClose() + }, + } + ) } - useEffect(() => { - if (visible) { - const { unit } = convertFromBytes(bucket?.file_size_limit ?? 0) - setSelectedUnit(unit) - setShowConfiguration(false) - } - }, [visible]) - return ( - { + if (!open) { + form.reset() + onClose() + } + }} > -
- {({ values, resetForm }: { values: any; resetForm: any }) => { - const isChangingBucketVisibility = bucket?.public !== values.public - const isMakingBucketPrivate = bucket?.public && !values.public - const isMakingBucketPublic = !bucket?.public && values.public - - // [Alaister] although this "technically" is breaking the rules of React hooks - // it won't error because the hooks are always rendered in the same order - // eslint-disable-next-line react-hooks/rules-of-hooks - useEffect(() => { - if (visible && bucket !== undefined) { - const { value: fileSizeLimit } = convertFromBytes(bucket.file_size_limit ?? 0) - - const values = { - name: bucket.name ?? '', - public: bucket.public, - file_size_limit: bucket.file_size_limit, - allowed_mime_types: (bucket.allowed_mime_types ?? []).join(', '), - - has_file_size_limit: bucket.file_size_limit !== null, - formatted_size_limit: fileSizeLimit ?? 0, - } - resetForm({ values, initialValues: values }) - } - }, [visible]) - - return ( - <> - - -
- + + {`Edit bucket "${bucket?.name}"`} + + + + + + ( + + + + + + )} + /> + ( + - {isChangingBucketVisibility && ( - -

- {isMakingBucketPublic - ? `This will make all objects in your bucket publicly accessible.` - : isMakingBucketPrivate - ? `All objects in your bucket will be private and only accessible via signed URLs, or downloaded with the right authorisation headers.` - : ''} -

+ description="Anyone can read any object without any authorization" + layout="flex" + > + + + +
+ )} + /> + {isChangingBucketVisibility && ( + + {isMakingBucketPublic ? ( +

`This will make all objects in your bucket publicly accessible.`

+ ) : isMakingBucketPrivate ? ( +

+ `All objects in your bucket will be private and only accessible via signed + URLs, or downloaded with the right authorisation headers.` +

+ ) : null} + {isMakingBucketPrivate && (

- Assets cached in the CDN may still be publicly accessible. You can - consider{' '} + { + 'Assets cached in the CDN may still be publicly accessible. You can consider ' + } purging the cache - {' '} - or moving your assets to a new bucket. + + {' or moving your assets to a new bucket.'}

)} -
- )} -
-
- + } + /> + )} + setShowConfiguration(!showConfiguration)} > - -
+ +
-
- -
-
- - {values.has_file_size_limit && ( -
-
- { - if (event.charCode < 48 || event.charCode > 57) { - event.preventDefault() - } - }} - descriptionText={`Equivalent to ${convertToBytes( - values.formatted_size_limit, - selectedUnit - ).toLocaleString()} bytes.`} + + + +
+ ( + + + -
-
- - {Object.values(StorageSizeUnits).map((unit: string) => ( - -
{unit}
-
- ))} -
-
- {IS_PLATFORM && ( -
-

- Note: Individual bucket upload will still be capped at the{' '} - - global upload limit - {' '} - of {formattedGlobalUploadLimit} -

-
- )} -
+ + )} -
- + {hasFileSizeLimit && ( +
+
+ ( + + + + + + )} + /> +
+ + + + <>{selectedUnit} + + + + {Object.values(StorageSizeUnits).map((unit: string) => ( + +
{unit}
+
+ ))} +
+
+ {IS_PLATFORM && ( +
+

+ Note: Individual bucket upload will still be capped at the{' '} + + global upload limit + {' '} + of {formattedGlobalUploadLimit} +

+
+ )} +
+ )}
- - - - - - - - - ) - }} - - + ( + + + + + + )} + /> + + + + + + + + + + + ) } diff --git a/apps/studio/components/interfaces/Storage/EmptyBucketModal.tsx b/apps/studio/components/interfaces/Storage/EmptyBucketModal.tsx index 77696bc4862..18e0e901eb9 100644 --- a/apps/studio/components/interfaces/Storage/EmptyBucketModal.tsx +++ b/apps/studio/components/interfaces/Storage/EmptyBucketModal.tsx @@ -4,7 +4,17 @@ import { toast } from 'sonner' import { useBucketEmptyMutation } from 'data/storage/bucket-empty-mutation' import type { Bucket } from 'data/storage/buckets-query' import { useStorageExplorerStateSnapshot } from 'state/storage-explorer' -import ConfirmationModal from 'ui-patterns/Dialogs/ConfirmationModal' +import { + Button, + Dialog, + DialogHeader, + DialogTitle, + DialogContent, + DialogSection, + DialogSectionSeparator, + DialogFooter, +} from 'ui' +import { Admonition } from 'ui-patterns' export interface EmptyBucketModalProps { visible: boolean @@ -12,7 +22,7 @@ export interface EmptyBucketModalProps { onClose: () => void } -export const EmptyBucketModal = ({ visible = false, bucket, onClose }: EmptyBucketModalProps) => { +export const EmptyBucketModal = ({ visible, bucket, onClose }: EmptyBucketModalProps) => { const { ref: projectRef } = useParams() const { fetchFolderContents } = useStorageExplorerStateSnapshot() @@ -37,21 +47,38 @@ export const EmptyBucketModal = ({ visible = false, bucket, onClose }: EmptyBuck } return ( - onClose()} - onConfirm={onEmptyBucket} - alert={{ - title: 'This action cannot be undone', - description: 'The contents of your bucket cannot be recovered once deleted', + { + if (!open) { + onClose() + } }} > -

Are you sure you want to empty the bucket "{bucket?.name}"?

-
+ + + {`Confirm to delete all contents from ${bucket?.name}`} + + + + +

Are you sure you want to empty the bucket "{bucket?.name}"?

+
+ + + + +
+ ) } + +export default EmptyBucketModal diff --git a/apps/studio/components/interfaces/Storage/StorageExplorer/FileExplorerRowEditing.tsx b/apps/studio/components/interfaces/Storage/StorageExplorer/FileExplorerRowEditing.tsx index cd17e1b1622..415e2b89cdb 100644 --- a/apps/studio/components/interfaces/Storage/StorageExplorer/FileExplorerRowEditing.tsx +++ b/apps/studio/components/interfaces/Storage/StorageExplorer/FileExplorerRowEditing.tsx @@ -2,7 +2,7 @@ import { has } from 'lodash' import { useEffect, useRef, useState } from 'react' import { useStorageExplorerStateSnapshot } from 'state/storage-explorer' -import { STORAGE_ROW_TYPES, STORAGE_VIEWS } from '../Storage.constants' +import { STORAGE_ROW_STATUS, STORAGE_ROW_TYPES, STORAGE_VIEWS } from '../Storage.constants' import { StorageItem } from '../Storage.types' import { RowIcon } from './FileExplorerRow' @@ -13,7 +13,8 @@ export interface FileExplorerRowEditingProps { } const FileExplorerRowEditing = ({ item, view, columnIndex }: FileExplorerRowEditingProps) => { - const { renameFile, renameFolder, addNewFolder } = useStorageExplorerStateSnapshot() + const { renameFile, renameFolder, addNewFolder, updateRowStatus } = + useStorageExplorerStateSnapshot() const inputRef = useRef(null) const [itemName, setItemName] = useState(item.name) @@ -28,7 +29,22 @@ const FileExplorerRowEditing = ({ item, view, columnIndex }: FileExplorerRowEdit await renameFile(item, name, columnIndex) } else if (has(item, 'id')) { const itemWithColumnIndex = { ...item, columnIndex } - renameFolder(itemWithColumnIndex, name, columnIndex) + renameFolder({ + folder: itemWithColumnIndex, + newName: name, + columnIndex, + onError: () => { + if (event.type === 'blur') { + updateRowStatus({ + name: itemWithColumnIndex.name, + status: STORAGE_ROW_STATUS.READY, + columnIndex, + }) + } else { + inputRef.current.select() + } + }, + }) } else { addNewFolder({ folderName: name, diff --git a/apps/studio/components/interfaces/Storage/StorageExplorer/StorageExplorer.tsx b/apps/studio/components/interfaces/Storage/StorageExplorer/StorageExplorer.tsx index 160e8118e4f..8871cbff00d 100644 --- a/apps/studio/components/interfaces/Storage/StorageExplorer/StorageExplorer.tsx +++ b/apps/studio/components/interfaces/Storage/StorageExplorer/StorageExplorer.tsx @@ -167,7 +167,7 @@ const StorageExplorer = ({ bucket }: StorageExplorerProps) => { return (
{selectedItems.length === 0 ? ( { const router = useRouter() const { ref, bucketId } = useParams() - const { data: project } = useSelectedProjectQuery() + const { data: projectDetails } = useSelectedProjectQuery() const snap = useStorageExplorerStateSnapshot() - const isBranch = project?.parent_project_ref !== undefined + const isBranch = projectDetails?.parent_project_ref !== undefined const [searchText, setSearchText] = useState('') - const [showCreateBucketModal, setShowCreateBucketModal] = useState(false) - const [selectedBucketToEdit, setSelectedBucketToEdit] = useState() - const [selectedBucketToEmpty, setSelectedBucketToEmpty] = useState() - const [selectedBucketToDelete, setSelectedBucketToDelete] = useState() - const canCreateBuckets = useCheckPermissions(PermissionAction.STORAGE_WRITE, '*') const page = router.pathname.split('/')[4] as | undefined @@ -69,24 +57,7 @@ const StorageMenu = () => { <>
- } - disabled={!canCreateBuckets} - style={{ justifyContent: 'start' }} - onClick={() => setShowCreateBucketModal(true)} - tooltip={{ - content: { - side: 'bottom', - text: !canCreateBuckets - ? 'You need additional permissions to create buckets' - : undefined, - }, - }} - > - New bucket - + { bucket={bucket} projectRef={ref} isSelected={isSelected} - onSelectEmptyBucket={() => setSelectedBucketToEmpty(bucket)} - onSelectDeleteBucket={() => setSelectedBucketToDelete(bucket)} - onSelectEditBucket={() => setSelectedBucketToEdit(bucket)} /> ) })} @@ -195,29 +163,6 @@ const StorageMenu = () => {
- - setShowCreateBucketModal(false)} - /> - - setSelectedBucketToEdit(undefined)} - /> - - setSelectedBucketToEmpty(undefined)} - /> - - setSelectedBucketToDelete(undefined)} - /> ) } diff --git a/apps/studio/components/interfaces/Storage/StoragePolicies/StoragePolicies.tsx b/apps/studio/components/interfaces/Storage/StoragePolicies/StoragePolicies.tsx index 0294019aa77..6f234884903 100644 --- a/apps/studio/components/interfaces/Storage/StoragePolicies/StoragePolicies.tsx +++ b/apps/studio/components/interfaces/Storage/StoragePolicies/StoragePolicies.tsx @@ -4,18 +4,23 @@ import { useState } from 'react' import { toast } from 'sonner' import PolicyEditorModal from 'components/interfaces/Auth/Policies/PolicyEditorModal' +import { + ScaffoldSection, + ScaffoldSectionDescription, + ScaffoldSectionTitle, +} from 'components/layouts/Scaffold' import { useDatabasePoliciesQuery } from 'data/database-policies/database-policies-query' import { useDatabasePolicyCreateMutation } from 'data/database-policies/database-policy-create-mutation' import { useDatabasePolicyDeleteMutation } from 'data/database-policies/database-policy-delete-mutation' import { useDatabasePolicyUpdateMutation } from 'data/database-policies/database-policy-update-mutation' import { useBucketsQuery } from 'data/storage/buckets-query' +import { useSelectedProjectQuery } from 'hooks/misc/useSelectedProject' import { Loader } from 'lucide-react' import ConfirmModal from 'ui-patterns/Dialogs/ConfirmDialog' import { formatPoliciesForStorage } from '../Storage.utils' import StoragePoliciesBucketRow from './StoragePoliciesBucketRow' import StoragePoliciesEditPolicyModal from './StoragePoliciesEditPolicyModal' import StoragePoliciesPlaceholder from './StoragePoliciesPlaceholder' -import { useSelectedProjectQuery } from 'hooks/misc/useSelectedProject' const StoragePolicies = () => { const { data: project } = useSelectedProjectQuery() @@ -176,67 +181,72 @@ const StoragePolicies = () => { return (
-

Storage policies

-

- Safeguard your files with policies that define the operations allowed for your users at the - bucket level. -

- {isLoading ? (
) : ( -
- {buckets.length === 0 && } +
+ + Buckets + + Write policies for each bucket to control access to the bucket and its contents + + {buckets.length === 0 && } - {/* Sections for policies grouped by buckets */} - {buckets.map((bucket) => { - const bucketPolicies = get( - find(formattedStorageObjectPolicies, { name: bucket.name }), - ['policies'], - [] - ).sort((a: any, b: any) => a.name.localeCompare(b.name)) + {/* Sections for policies grouped by buckets */} +
+ {buckets.map((bucket) => { + const bucketPolicies = get( + find(formattedStorageObjectPolicies, { name: bucket.name }), + ['policies'], + [] + ).sort((a: any, b: any) => a.name.localeCompare(b.name)) - return ( + return ( + + ) + })} +
+
+ + + Schema + + Write policies for the tables under the storage schema directly for greater control + + +
+ {/* Section for policies under storage.objects that are not tied to any buckets */} - ) - })} -
-

- You may also write policies for the tables under the storage schema directly for greater - control -

- - {/* Section for policies under storage.objects that are not tied to any buckets */} - - - {/* Section for policies under storage.buckets */} - + {/* Section for policies under storage.buckets */} + +
+
)} diff --git a/apps/studio/components/interfaces/Storage/StoragePolicies/StoragePoliciesBucketRow.tsx b/apps/studio/components/interfaces/Storage/StoragePolicies/StoragePoliciesBucketRow.tsx index 178141b7c34..26eb07822ce 100644 --- a/apps/studio/components/interfaces/Storage/StoragePolicies/StoragePoliciesBucketRow.tsx +++ b/apps/studio/components/interfaces/Storage/StoragePolicies/StoragePoliciesBucketRow.tsx @@ -4,6 +4,10 @@ import { Archive, Edit, MoreVertical, Trash } from 'lucide-react' import { Badge, Button, + Card, + CardContent, + CardHeader, + CardTitle, DropdownMenu, DropdownMenuContent, DropdownMenuItem, @@ -28,35 +32,33 @@ const PolicyRow = ({ }: PolicyRowProps) => { const { name, command } = policy return ( -
- -
-
{command}
-
- {name} -
+ +
+
{command}
+
+ {name}
- - -
+
+ + + -
, - ]} - > - {policies.length === 0 ? ( -
-

No policies created yet

+ + +
+ + {label} + {bucket.public && Public}
+ +
+ {policies.length === 0 ? ( + +

No policies created yet

+
) : ( -
+
{policies.map((policy: any) => ( ))} - {policies.length !== 0 ? ( -
-

{getFooterLabel()}

-
- ) : null}
)} - + ) } diff --git a/apps/studio/components/interfaces/Storage/StoragePolicies/StoragePoliciesPlaceholder.tsx b/apps/studio/components/interfaces/Storage/StoragePolicies/StoragePoliciesPlaceholder.tsx index 4dd36cabfa9..50bee986b54 100644 --- a/apps/studio/components/interfaces/Storage/StoragePolicies/StoragePoliciesPlaceholder.tsx +++ b/apps/studio/components/interfaces/Storage/StoragePolicies/StoragePoliciesPlaceholder.tsx @@ -1,23 +1,13 @@ -import Panel from 'components/ui/Panel' -import { Archive } from 'lucide-react' +import { CardContent, Card } from 'ui' const StoragePoliciesPlaceholder = () => ( - -
- -

Bucket policies

-
-
, - ]} - > -
+ +

Create a bucket first to start writing policies!

-
- + +
) export default StoragePoliciesPlaceholder diff --git a/apps/studio/components/interfaces/Storage/StorageSettings/S3Connection.tsx b/apps/studio/components/interfaces/Storage/StorageSettings/S3Connection.tsx index e8fbe4a986a..eb16fee9daa 100644 --- a/apps/studio/components/interfaces/Storage/StorageSettings/S3Connection.tsx +++ b/apps/studio/components/interfaces/Storage/StorageSettings/S3Connection.tsx @@ -9,11 +9,15 @@ import * as z from 'zod' import { useParams } from 'common' import { useIsProjectActive } from 'components/layouts/ProjectLayout/ProjectContext' +import { + ScaffoldSection, + ScaffoldSectionDescription, + ScaffoldSectionTitle, +} from 'components/layouts/Scaffold' import Table from 'components/to-be-cleaned/Table' import AlertError from 'components/ui/AlertError' -import { FormHeader } from 'components/ui/Forms/FormHeader' +import { DocsButton } from 'components/ui/DocsButton' import NoPermission from 'components/ui/NoPermission' -import Panel from 'components/ui/Panel' import { useProjectSettingsV2Query } from 'data/config/project-settings-v2-query' import { useProjectStorageConfigQuery } from 'data/config/project-storage-config-query' import { useProjectStorageConfigUpdateUpdateMutation } from 'data/config/project-storage-config-update-mutation' @@ -24,10 +28,12 @@ import { AlertDescription_Shadcn_, Alert_Shadcn_, Button, + Card, + CardContent, + CardFooter, FormControl_Shadcn_, FormField_Shadcn_, Form_Shadcn_, - Separator, Switch, WarningIcon, } from 'ui' @@ -102,90 +108,87 @@ export const S3Connection = () => { return ( <> - -
- - {projectIsLoading ? ( - - ) : isProjectActive ? ( - - ( - - - - - - )} - /> - - {isErrorStorageConfig && ( -
- +
+
+ S3 Connection + + Connect to your bucket using any S3-compatible service via the S3 protocol + +
+ +
+ + + {projectIsLoading ? ( + + ) : isProjectActive ? ( + + + ( + + + + + + )} /> -
- )} - + {isErrorStorageConfig && ( +
+ +
+ )} + -
- - - - {!projectIsLoading && ( - - - + +
+ + + + {!projectIsLoading && ( + + + + )} +
+
+ + {!canUpdateStorageSettings && ( + +

+ You need additional permissions to update storage settings +

+
)} -
- - -
- {!canUpdateStorageSettings ? ( -

- You need additional permissions to update storage settings -

- ) : ( -
- )} -
- + + {form.formState.isDirty && ( + + )} + + + ) : ( + + + Project is paused + + To connect to your S3 bucket, you need to restore your project. + +
+
-
- - ) : ( - - - Project is paused - - To connect to your S3 bucket, you need to restore your project. - -
- -
-
- )} - - - -
- - } - /> + + )} + + + + +
+
+ S3 Access Keys + + Manage your access keys for this project. + +
+ +
{!canReadS3Credentials ? ( @@ -286,7 +290,7 @@ export const S3Connection = () => { )} )} -
+ { } }, [organization]) - const formattedMaxSizeBytes = `${new Intl.NumberFormat('en-US').format(maxBytes)} bytes` - const FormSchema = z .object({ fileSizeLimit: z.coerce.number(), @@ -156,55 +156,50 @@ const StorageSettings = () => { } return ( - - {isLoading && } - {isError && ( - - )} - {isSuccess && ( -
-
-
-
-
-

Upload file size limit

-
- -
-
-
- ( - - - size - - - - - - - )} - /> -
-
- ( - - Unit - + + + {isLoading && } + {isError && ( + + )} + {isSuccess && ( + + + + ( + + {storageUnit !== StorageSizeUnits.BYTES && ( + <> + Equivalent to {convertToBytes(limit, storageUnit).toLocaleString()}{' '} + bytes.{' '} + + )} + Maximum upload file size is {formatBytes(maxBytes)}. + + } + > + +
+ + ( @@ -224,84 +219,80 @@ const StorageSettings = () => { ))} - - - - )} - /> -
-
-

- {storageUnit !== StorageSizeUnits.BYTES && - `Equivalent to ${convertToBytes( - limit, - storageUnit - ).toLocaleString()} bytes. `} - Maximum upload file size is {formatBytes(maxBytes)}. -

-
-
+ )} + /> +
+ + + )} + /> + -
-
-

Enable Image Transformation

-
-
-
- ( + + ( + + Optimize and resize images on the fly.{' '} + + Learn more + + . + + } + > + - )} - /> -
-

- Optimize and resize images on the fly.{' '} - - Learn more - - . -

-
-
-
+ + + )} + /> + - {isFreeTier && ( -
- -
- )} - {isSpendCapOn && ( -
- -
- )} -
-
-
- {!canUpdateStorageSettings ? ( + {isFreeTier && ( + + + + )} + {isSpendCapOn && ( + + + + )} + + {!canUpdateStorageSettings && ( +

You need additional permissions to update storage settings

- ) : ( -
- )} -
+ + )} + + + {form.formState.isDirty && ( - -
-
-
-
- - )} - + )} + + + + + )} + + ) } diff --git a/apps/studio/components/interfaces/Storage/__tests__/CreateBucketModal.test.tsx b/apps/studio/components/interfaces/Storage/__tests__/CreateBucketModal.test.tsx new file mode 100644 index 00000000000..b0c4c7fc7b9 --- /dev/null +++ b/apps/studio/components/interfaces/Storage/__tests__/CreateBucketModal.test.tsx @@ -0,0 +1,100 @@ +import { describe, expect, it, beforeEach, vi } from 'vitest' +import { screen, waitFor, fireEvent } from '@testing-library/dom' +import userEvent from '@testing-library/user-event' + +import { addAPIMock } from 'tests/lib/msw' +import { ProjectContextProvider } from 'components/layouts/ProjectLayout/ProjectContext' +import { render } from 'tests/helpers' +import { routerMock } from 'tests/lib/route-mock' +import CreateBucketModal from '../CreateBucketModal' + +describe(`CreateBucketModal`, () => { + beforeEach(() => { + vi.mock(`hooks/misc/useCheckPermissions`, () => ({ + useCheckPermissions: vi.fn(), + useAsyncCheckProjectPermissions: vi.fn().mockImplementation(() => ({ can: true })), + })) + // useParams + routerMock.setCurrentUrl(`/project/default/storage/buckets`) + // useSelectedProject -> Project + addAPIMock({ + method: `get`, + path: `/platform/projects/:ref`, + // @ts-expect-error + response: { + cloud_provider: 'localhost', + id: 1, + inserted_at: '2021-08-02T06:40:40.646Z', + name: 'Default Project', + organization_id: 1, + ref: 'default', + region: 'local', + status: 'ACTIVE_HEALTHY', + }, + }) + // useBucketCreateMutation + addAPIMock({ + method: `post`, + path: `/platform/storage/:ref/buckets`, + }) + }) + + it(`renders a dialog with a form`, async () => { + render( + + + + ) + + const dialogTrigger = screen.getByRole(`button`, { name: `New bucket` }) + await userEvent.click(dialogTrigger) + + await waitFor(() => { + expect(screen.getByRole(`dialog`)).toBeInTheDocument() + }) + + const nameInput = screen.getByLabelText(`Name of bucket`) + await userEvent.type(nameInput, `test`) + + const standardOption = screen.getByLabelText(`Standard bucket`) + await userEvent.click(standardOption) + + const publicToggle = screen.getByLabelText(`Public bucket`) + expect(publicToggle).not.toBeChecked() + await userEvent.click(publicToggle) + expect(publicToggle).toBeChecked() + + const detailsTrigger = screen.getByRole(`button`, { name: `Additional configuration` }) + expect(detailsTrigger).toHaveAttribute(`data-state`, `closed`) + await userEvent.click(detailsTrigger) + expect(detailsTrigger).toHaveAttribute(`data-state`, `open`) + + const sizeLimitToggle = screen.getByLabelText(`Restrict file upload size for bucket`) + expect(sizeLimitToggle).not.toBeChecked() + await userEvent.click(sizeLimitToggle) + expect(sizeLimitToggle).toBeChecked() + + const sizeLimitInput = screen.getByLabelText(`File size limit`) + expect(sizeLimitInput).toHaveValue(0) + await userEvent.type(sizeLimitInput, `25`) + + const sizeLimitUnitSelect = screen.getByLabelText(`File size limit unit`) + expect(sizeLimitUnitSelect).toHaveTextContent(`bytes`) + await userEvent.click(sizeLimitUnitSelect) + const mbOption = screen.getByRole(`option`, { name: `MB` }) + await userEvent.click(mbOption) + expect(sizeLimitUnitSelect).toHaveTextContent(`MB`) + + const mimeTypeInput = screen.getByLabelText(`Allowed MIME types`) + expect(mimeTypeInput).toHaveValue(``) + await userEvent.type(mimeTypeInput, `image/jpeg, image/png`) + + const submitButton = screen.getByRole(`button`, { name: `Create` }) + + fireEvent.click(submitButton) + + await waitFor(() => + expect(routerMock.asPath).toStrictEqual(`/project/default/storage/buckets/test`) + ) + }) +}) diff --git a/apps/studio/components/interfaces/Storage/__tests__/DeleteBucketModal.test.tsx b/apps/studio/components/interfaces/Storage/__tests__/DeleteBucketModal.test.tsx new file mode 100644 index 00000000000..ea63c932272 --- /dev/null +++ b/apps/studio/components/interfaces/Storage/__tests__/DeleteBucketModal.test.tsx @@ -0,0 +1,141 @@ +import { describe, expect, it, beforeEach, vi } from 'vitest' +import { screen, waitFor, fireEvent } from '@testing-library/dom' +import userEvent from '@testing-library/user-event' +import { useState } from 'react' +import { faker } from '@faker-js/faker' + +import { addAPIMock } from 'tests/lib/msw' +import { ProjectContextProvider } from 'components/layouts/ProjectLayout/ProjectContext' +import { Bucket } from 'data/storage/buckets-query' +import DeleteBucketModal from '../DeleteBucketModal' +import { render } from 'tests/helpers' +import { routerMock } from 'tests/lib/route-mock' + +const bucket: Bucket = { + id: faker.string.uuid(), + name: `test`, + owner: faker.string.uuid(), + public: faker.datatype.boolean(), + allowed_mime_types: faker.helpers.multiple(() => faker.system.mimeType(), { + count: { min: 1, max: 5 }, + }), + file_size_limit: faker.number.int({ min: 0, max: 25165824 }), + type: faker.helpers.arrayElement(['STANDARD', 'ANALYTICS', undefined]), + created_at: faker.date.recent().toISOString(), + updated_at: faker.date.recent().toISOString(), +} + +const Page = ({ onClose }: { onClose: () => void }) => { + const [open, setOpen] = useState(false) + return ( + + + + { + setOpen(false) + onClose() + }} + /> + + ) +} + +describe(`DeleteBucketModal`, () => { + beforeEach(() => { + // useParams + routerMock.setCurrentUrl(`/project/default/storage/buckets/test`) + // useProjectContext + addAPIMock({ + method: `get`, + path: `/platform/projects/:ref`, + // @ts-expect-error + response: { + cloud_provider: 'localhost', + id: 1, + inserted_at: '2021-08-02T06:40:40.646Z', + name: 'Default Project', + organization_id: 1, + ref: 'default', + region: 'local', + status: 'ACTIVE_HEALTHY', + }, + }) + // useBucketsQuery + addAPIMock({ + method: `get`, + path: `/platform/storage/:ref/buckets`, + response: [bucket], + }) + // useDatabasePoliciesQuery + addAPIMock({ + method: `get`, + path: `/platform/pg-meta/:ref/policies`, + response: [ + { + id: faker.number.int({ min: 1 }), + name: faker.word.noun(), + action: faker.helpers.arrayElement(['PERMISSIVE', 'RESTRICTIVE']), + command: faker.helpers.arrayElement(['SELECT', 'INSERT', 'UPDATE', 'DELETE', 'ALL']), + table: faker.word.noun(), + table_id: faker.number.int({ min: 1 }), + check: null, + definition: null, + schema: faker.lorem.sentence(), + roles: faker.helpers.multiple(() => faker.word.noun(), { + count: { min: 1, max: 5 }, + }), + }, + ], + }) + // useBucketDeleteMutation + addAPIMock({ + method: `post`, + path: `/platform/storage/:ref/buckets/:id/empty`, + }) + // useDatabasePolicyDeleteMutation + addAPIMock({ + method: `delete`, + path: `/platform/storage/:ref/buckets/:id`, + }) + }) + + it(`renders a confirmation dialog`, async () => { + const onClose = vi.fn() + render() + + const openButton = screen.getByRole(`button`, { name: `Open` }) + await userEvent.click(openButton) + await screen.findByRole(`dialog`) + + const input = screen.getByLabelText(/Type/) + await userEvent.type(input, `test`) + + const confirmButton = screen.getByRole(`button`, { name: `Delete Bucket` }) + fireEvent.click(confirmButton) + + await waitFor(() => expect(onClose).toHaveBeenCalledOnce()) + expect(routerMock.asPath).toStrictEqual(`/project/default/storage/buckets`) + }) + + it(`prevents submission when the input doesn't match the bucket name`, async () => { + const onClose = vi.fn() + render() + + const openButton = screen.getByRole(`button`, { name: `Open` }) + await userEvent.click(openButton) + await screen.findByRole(`dialog`) + + const input = screen.getByLabelText(/Type/) + await userEvent.type(input, `invalid`) + + const confirmButton = screen.getByRole(`button`, { name: `Delete Bucket` }) + fireEvent.click(confirmButton) + + await waitFor(() => { + expect(screen.getByText(/Please enter/)).toBeInTheDocument() + }) + }) +}) diff --git a/apps/studio/components/interfaces/Storage/__tests__/EditBucketModal.test.tsx b/apps/studio/components/interfaces/Storage/__tests__/EditBucketModal.test.tsx new file mode 100644 index 00000000000..4824277d557 --- /dev/null +++ b/apps/studio/components/interfaces/Storage/__tests__/EditBucketModal.test.tsx @@ -0,0 +1,119 @@ +import { describe, expect, it, beforeEach, vi } from 'vitest' +import { screen, waitFor, fireEvent } from '@testing-library/dom' +import userEvent from '@testing-library/user-event' +import { useState } from 'react' +import { faker } from '@faker-js/faker' + +import { addAPIMock } from 'tests/lib/msw' +import { ProjectContextProvider } from 'components/layouts/ProjectLayout/ProjectContext' +import { Bucket } from 'data/storage/buckets-query' +import { render } from 'tests/helpers' +import { routerMock } from 'tests/lib/route-mock' +import EditBucketModal from '../EditBucketModal' + +const bucket: Bucket = { + id: faker.string.uuid(), + name: `test`, + owner: faker.string.uuid(), + public: false, + allowed_mime_types: [], + file_size_limit: undefined, + type: 'STANDARD', + created_at: faker.date.recent().toISOString(), + updated_at: faker.date.recent().toISOString(), +} + +const Page = ({ onClose }: { onClose: () => void }) => { + const [open, setOpen] = useState(false) + return ( + + + + { + setOpen(false) + onClose() + }} + /> + + ) +} + +describe(`EditBucketModal`, () => { + beforeEach(() => { + // useParams + routerMock.setCurrentUrl(`/project/default/storage/buckets/test`) + // useSelectedProject -> Project + addAPIMock({ + method: `get`, + path: `/platform/projects/:ref`, + // @ts-expect-error + response: { + cloud_provider: 'localhost', + id: 1, + inserted_at: '2021-08-02T06:40:40.646Z', + name: 'Default Project', + organization_id: 1, + ref: 'default', + region: 'local', + status: 'ACTIVE_HEALTHY', + }, + }) + // useBucketUpdateMutation + addAPIMock({ + method: `patch`, + path: `/platform/storage/:ref/buckets/:id`, + }) + }) + + it(`renders a dialog with a form`, async () => { + const onClose = vi.fn() + render() + + const openButton = screen.getByRole(`button`, { name: `Open` }) + await userEvent.click(openButton) + await screen.findByRole(`dialog`) + + const nameInput = screen.getByLabelText(`Name of bucket`) + expect(nameInput).toHaveValue(`test`) + expect(nameInput).toBeDisabled() + + const publicToggle = screen.getByLabelText(`Public bucket`) + expect(publicToggle).not.toBeChecked() + await userEvent.click(publicToggle) + expect(publicToggle).toBeChecked() + + const detailsTrigger = screen.getByRole(`button`, { name: `Additional configuration` }) + expect(detailsTrigger).toHaveAttribute(`data-state`, `closed`) + await userEvent.click(detailsTrigger) + expect(detailsTrigger).toHaveAttribute(`data-state`, `open`) + + const sizeLimitToggle = screen.getByLabelText(`Restrict file upload size for bucket`) + expect(sizeLimitToggle).not.toBeChecked() + await userEvent.click(sizeLimitToggle) + expect(sizeLimitToggle).toBeChecked() + + const sizeLimitInput = screen.getByLabelText(`File size limit`) + expect(sizeLimitInput).toHaveValue(0) + await userEvent.type(sizeLimitInput, `25`) + + const sizeLimitUnitSelect = screen.getByLabelText(`File size limit unit`) + expect(sizeLimitUnitSelect).toHaveTextContent(`bytes`) + await userEvent.click(sizeLimitUnitSelect) + const mbOption = screen.getByRole(`option`, { name: `MB` }) + await userEvent.click(mbOption) + expect(sizeLimitUnitSelect).toHaveTextContent(`MB`) + + const mimeTypeInput = screen.getByLabelText(`Allowed MIME types`) + expect(mimeTypeInput).toHaveValue(``) + await userEvent.type(mimeTypeInput, `image/jpeg, image/png`) + + const confirmButton = screen.getByRole(`button`, { name: `Save` }) + + fireEvent.click(confirmButton) + + await waitFor(() => expect(onClose).toHaveBeenCalledOnce()) + }) +}) diff --git a/apps/studio/components/interfaces/Storage/__tests__/EmptyBucketModal.test.tsx b/apps/studio/components/interfaces/Storage/__tests__/EmptyBucketModal.test.tsx new file mode 100644 index 00000000000..85762108745 --- /dev/null +++ b/apps/studio/components/interfaces/Storage/__tests__/EmptyBucketModal.test.tsx @@ -0,0 +1,96 @@ +import { describe, expect, it, beforeEach, vi } from 'vitest' +import { screen, waitFor, fireEvent } from '@testing-library/dom' +import userEvent from '@testing-library/user-event' +import { useState } from 'react' +import { faker } from '@faker-js/faker' + +import { addAPIMock } from 'tests/lib/msw' +import { ProjectContextProvider } from 'components/layouts/ProjectLayout/ProjectContext' +import { Bucket } from 'data/storage/buckets-query' +import EmptyBucketModal from '../EmptyBucketModal' +import { render } from 'tests/helpers' +import { routerMock } from 'tests/lib/route-mock' + +const bucket: Bucket = { + id: faker.string.uuid(), + name: `test`, + owner: faker.string.uuid(), + public: faker.datatype.boolean(), + allowed_mime_types: faker.helpers.multiple(() => faker.system.mimeType(), { + count: { min: 1, max: 5 }, + }), + file_size_limit: faker.number.int({ min: 0, max: 25165824 }), + type: faker.helpers.arrayElement(['STANDARD', 'ANALYTICS', undefined]), + created_at: faker.date.recent().toISOString(), + updated_at: faker.date.recent().toISOString(), +} + +const Page = ({ onClose }: { onClose: () => void }) => { + const [open, setOpen] = useState(false) + return ( + + + + { + setOpen(false) + onClose() + }} + /> + + ) +} + +describe(`EmptyBucketModal`, () => { + beforeEach(() => { + // useParams + routerMock.setCurrentUrl(`/project/default/storage/buckets/test`) + // useSelectedProject -> Project + addAPIMock({ + method: `get`, + path: `/platform/projects/:ref`, + // @ts-expect-error + response: { + cloud_provider: 'localhost', + id: 1, + inserted_at: '2021-08-02T06:40:40.646Z', + name: 'Default Project', + organization_id: 1, + ref: 'default', + region: 'local', + status: 'ACTIVE_HEALTHY', + }, + }) + // useBucketEmptyMutation + addAPIMock({ + method: `post`, + path: `/platform/storage/:ref/buckets/:id/empty`, + }) + // Called by useStorageExplorerStateSnapshot but seems + // to be unnecessary for succesful test? + // + // useProjectSettingsV2Query -> ProjectSettings + // GET /platform/projects/:ref/settings + // useAPIKeysQuery -> APIKey[] + // GET /v1/projects/:ref/api-keys + // listBucketObjects -> ListBucketObjectsData + // POST /platform/storage/:ref/buckets/:id/objects/list + }) + + it(`renders a confirmation dialog`, async () => { + const onClose = vi.fn() + render() + + const openButton = screen.getByRole(`button`, { name: `Open` }) + await userEvent.click(openButton) + await screen.findByRole(`dialog`) + + const confirmButton = screen.getByRole(`button`, { name: `Empty Bucket` }) + + fireEvent.click(confirmButton) + + await waitFor(() => expect(onClose).toHaveBeenCalledOnce()) + }) +}) diff --git a/apps/studio/components/interfaces/TableGridEditor/GridHeaderActions.tsx b/apps/studio/components/interfaces/TableGridEditor/GridHeaderActions.tsx index f43ac43e38a..fb4b6348f36 100644 --- a/apps/studio/components/interfaces/TableGridEditor/GridHeaderActions.tsx +++ b/apps/studio/components/interfaces/TableGridEditor/GridHeaderActions.tsx @@ -26,6 +26,7 @@ import { useIsFeatureEnabled } from 'hooks/misc/useIsFeatureEnabled' import { useSelectedOrganizationQuery } from 'hooks/misc/useSelectedOrganization' import { useSelectedProjectQuery } from 'hooks/misc/useSelectedProject' import { useIsProtectedSchema } from 'hooks/useProtectedSchemas' +import { parseAsBoolean, useQueryState } from 'nuqs' import { useTableEditorTableStateSnapshot } from 'state/table-editor-table' import { Button, @@ -51,6 +52,11 @@ const GridHeaderActions = ({ table }: GridHeaderActionsProps) => { const { data: project } = useSelectedProjectQuery() const { data: org } = useSelectedOrganizationQuery() + const [showWarning, setShowWarning] = useQueryState( + 'showWarning', + parseAsBoolean.withDefault(false) + ) + // need project lints to get security status for views const { data: lints = [] } = useProjectLintsQuery({ projectRef: project?.ref }) @@ -261,7 +267,7 @@ const GridHeaderActions = ({ table }: GridHeaderActionsProps) => { )} ) : ( - + -
, - { duration: Infinity } - ) - } else { + // Process a file into table rows and columns (used for both upload and drop) + const processFile = useCallback( + async (file: File) => { updateEditorDirty(true) setUploadedFile(file) + setParseProgress(0) + const { headers, rowCount, columnTypeMap, errors, previewRows } = await parseSpreadsheet( file, onProgressUpdate ) + if (errors.length > 0) { toast.error( `Some issues have been detected on ${errors.length} rows. More details below the content preview.` @@ -115,17 +98,31 @@ const SpreadsheetImport = ({ setErrors(errors) setSelectedHeaders(headers) setSpreadsheetData({ headers, rows: previewRows, rowCount, columnTypeMap }) - } - event.target.value = '' - } + }, + [updateEditorDirty] + ) - const resetSpreadsheetImport = () => { + // Handle file upload events from file input + const onFileUpload = useCallback( + async (event: any) => { + event.persist() + const [file] = event.target.files || event.dataTransfer.files + if (file && !flagInvalidFileImport(file)) { + await processFile(file) + } else { + event.target.value = '' + } + }, + [processFile] + ) + + const resetSpreadsheetImport = useCallback(() => { setInput('') setSpreadsheetData(EMPTY_SPREADSHEET_DATA) setUploadedFile(undefined) setErrors([]) updateEditorDirty(false) - } + }, [updateEditorDirty]) const readSpreadsheetText = async (text: string) => { if (text.length > 0) { @@ -143,6 +140,7 @@ const SpreadsheetImport = ({ } } + // eslint-disable-next-line react-hooks/exhaustive-deps const handler = useCallback(debounce(readSpreadsheetText, debounceDuration), []) const onInputChange = (event: any) => { setInput(event.target.value) @@ -178,8 +176,18 @@ const SpreadsheetImport = ({ } useEffect(() => { - if (visible && headers.length === 0) resetSpreadsheetImport() - }, [visible]) + if (visiblityChanged && visible) { + if (fileFromState) processFile(fileFromState) + else if (headers.length === 0) resetSpreadsheetImport() + } + }, [ + visiblityChanged, + visible, + fileFromState, + processFile, + headers.length, + resetSpreadsheetImport, + ]) return ( { const ext = file?.name.split('.').pop().toLowerCase() return UPLOAD_FILE_EXTENSIONS.includes(ext) } + +export function flagInvalidFileImport(file: File): boolean { + if (!file || !UPLOAD_FILE_TYPES.includes(file.type) || !acceptedFileExtension(file)) { + toast.error("Couldn't import file: only CSV files are accepted") + return true + } else if (file.size > MAX_TABLE_EDITOR_IMPORT_CSV_SIZE) { + toast.error( +
+

The dashboard currently only supports importing of CSVs below 100MB.

+

For bulk data loading, we recommend doing so directly through the database.

+ +
, + { duration: Infinity } + ) + return true + } + + return false +} diff --git a/apps/studio/components/layouts/LinkAwsMarketplaceLayout.tsx b/apps/studio/components/layouts/LinkAwsMarketplaceLayout.tsx new file mode 100644 index 00000000000..8baff253240 --- /dev/null +++ b/apps/studio/components/layouts/LinkAwsMarketplaceLayout.tsx @@ -0,0 +1,51 @@ +import { BASE_PATH } from 'lib/constants' +import { useTheme } from 'next-themes' +import Head from 'next/head' +import Image from 'next/legacy/image' +import type { PropsWithChildren } from 'react' +import { Separator } from 'ui' +import { withAuth } from '../../hooks/misc/withAuth' + +export interface LinkAwsMarketplaceLayoutProps {} + +const LinkAwsMarketplaceLayout = ({ + children, +}: PropsWithChildren) => { + const { resolvedTheme } = useTheme() + return ( + <> + + AWS Marketplace Setup | Supabase + +
+
+
+
+
+
+ Supabase + Supabase Logo +
+
+
+
+
+ +
+ {children} +
+
+ + ) +} + +export default withAuth(LinkAwsMarketplaceLayout) diff --git a/apps/studio/components/layouts/LogsLayout/LogsLayout.tsx b/apps/studio/components/layouts/LogsLayout/LogsLayout.tsx index 2edee7ba163..30c4ae8bbf4 100644 --- a/apps/studio/components/layouts/LogsLayout/LogsLayout.tsx +++ b/apps/studio/components/layouts/LogsLayout/LogsLayout.tsx @@ -1,11 +1,9 @@ import { PermissionAction } from '@supabase/shared-types/out/constants' -import { useRouter } from 'next/router' -import { PropsWithChildren, useEffect } from 'react' -import { LOCAL_STORAGE_KEYS } from 'common' +import { PropsWithChildren } from 'react' + import NoPermission from 'components/ui/NoPermission' import { useAsyncCheckProjectPermissions } from 'hooks/misc/useCheckPermissions' -import { useLocalStorageQuery } from 'hooks/misc/useLocalStorage' import { withAuth } from 'hooks/misc/withAuth' import ProjectLayout from '../ProjectLayout/ProjectLayout' import { LogsSidebarMenuV2 } from './LogsSidebarMenuV2' @@ -20,21 +18,6 @@ const LogsLayout = ({ title, children }: PropsWithChildren) => 'logflare' ) - const router = useRouter() - const [_, setLastLogsPage] = useLocalStorageQuery( - LOCAL_STORAGE_KEYS.LAST_VISITED_LOGS_PAGE, - router.pathname.split('/logs/')[1] || '' - ) - - useEffect(() => { - if (router.pathname.includes('/logs/')) { - const path = router.pathname.split('/logs/')[1] - if (path) { - setLastLogsPage(path) - } - } - }, [router, setLastLogsPage]) - if (!canUseLogsExplorer) { if (isLoading) { return diff --git a/apps/studio/components/layouts/ProjectLayout/LoadingState.tsx b/apps/studio/components/layouts/ProjectLayout/LoadingState.tsx index 122ff258752..25f46fb6bdb 100644 --- a/apps/studio/components/layouts/ProjectLayout/LoadingState.tsx +++ b/apps/studio/components/layouts/ProjectLayout/LoadingState.tsx @@ -13,23 +13,25 @@ const LoadingState = () => { return (
-
- {isLoading ? ( - - ) : ( -

{projectName}

- )} +
+
+ {isLoading ? ( + + ) : ( +

{projectName}

+ )} +
- -
- -
- +
+
+ +
- -
- - +
+
+ + +
) diff --git a/apps/studio/components/layouts/ProjectLayout/NavigationBar/NavigationBar.utils.tsx b/apps/studio/components/layouts/ProjectLayout/NavigationBar/NavigationBar.utils.tsx index 48faa8be673..503d3c8d973 100644 --- a/apps/studio/components/layouts/ProjectLayout/NavigationBar/NavigationBar.utils.tsx +++ b/apps/studio/components/layouts/ProjectLayout/NavigationBar/NavigationBar.utils.tsx @@ -116,10 +116,16 @@ export const generateProductRoutes = ( ] } -export const generateOtherRoutes = (ref?: string, project?: Project, features?: {}): Route[] => { +export const generateOtherRoutes = ( + ref?: string, + project?: Project, + features?: { unifiedLogs?: boolean } +): Route[] => { const isProjectBuilding = project?.status === PROJECT_STATUS.COMING_UP const buildingUrl = `/project/${ref}` + const unifiedLogsEnabled = features?.unifiedLogs ?? false + return [ { key: 'advisors', @@ -141,7 +147,13 @@ export const generateOtherRoutes = (ref?: string, project?: Project, features?: key: 'logs', label: 'Logs', icon: , - link: ref && (isProjectBuilding ? buildingUrl : `/project/${ref}/logs`), + link: + ref && + (isProjectBuilding + ? buildingUrl + : unifiedLogsEnabled + ? `/project/${ref}/logs` + : `/project/${ref}/logs/explorer`), }, { key: 'api', diff --git a/apps/studio/components/layouts/TableEditorLayout/EntityListItem.tsx b/apps/studio/components/layouts/TableEditorLayout/EntityListItem.tsx index c2e9ebd021b..f835be98f68 100644 --- a/apps/studio/components/layouts/TableEditorLayout/EntityListItem.tsx +++ b/apps/studio/components/layouts/TableEditorLayout/EntityListItem.tsx @@ -4,7 +4,7 @@ import Link from 'next/link' import Papa from 'papaparse' import { toast } from 'sonner' -import { IS_PLATFORM } from 'common' +import { IS_PLATFORM, useParams } from 'common' import { MAX_EXPORT_ROW_COUNT, MAX_EXPORT_ROW_COUNT_MESSAGE, @@ -16,6 +16,7 @@ import { } from 'components/interfaces/TableGridEditor/TableEntity.utils' import { EntityTypeIcon } from 'components/ui/EntityTypeIcon' import type { ItemRenderer } from 'components/ui/InfiniteList' +import { InlineLink } from 'components/ui/InlineLink' import { getTableDefinition } from 'data/database/table-definition-query' import { ENTITY_TYPE } from 'data/entity-types/entity-type-constants' import { Entity } from 'data/entity-types/entity-types-infinite-query' @@ -453,28 +454,53 @@ const EntityTooltipTrigger = ({ materializedViewHasLints: boolean foreignTableHasLints: boolean }) => { - let tooltipContent = '' + const { ref } = useParams() + + let tooltipContent = null const accessWarning = 'Data is publicly accessible via API' + const learnMoreCTA = ( + + Learn more + + ) switch (entity.type) { case ENTITY_TYPE.TABLE: if (tableHasLints) { - tooltipContent = `${accessWarning} as RLS is disabled` + tooltipContent = ( + <> + {accessWarning} as RLS is disabled. {learnMoreCTA}. + + ) } break case ENTITY_TYPE.VIEW: if (viewHasLints) { - tooltipContent = `${accessWarning} as this is a Security definer view` + tooltipContent = ( + <> + {accessWarning} as this is a Security definer view. {learnMoreCTA}. + + ) } break case ENTITY_TYPE.MATERIALIZED_VIEW: if (materializedViewHasLints) { - tooltipContent = `${accessWarning} Security definer view` + tooltipContent = ( + <> + {accessWarning} as this is a Security definer view {learnMoreCTA}. + + ) } break case ENTITY_TYPE.FOREIGN_TABLE: if (foreignTableHasLints) { - tooltipContent = `${accessWarning} as RLS is not enforced on foreign tables` + tooltipContent = ( + <> + {accessWarning} as RLS is not enforced on foreign tables. {learnMoreCTA}. + + ) } break default: @@ -483,12 +509,12 @@ const EntityTooltipTrigger = ({ if (tooltipContent) { return ( - + Unrestricted - - {tooltipContent} + + {tooltipContent} ) diff --git a/apps/studio/components/layouts/Tabs/Tabs.utils.ts b/apps/studio/components/layouts/Tabs/Tabs.utils.ts index de63834fda0..f67aea00456 100644 --- a/apps/studio/components/layouts/Tabs/Tabs.utils.ts +++ b/apps/studio/components/layouts/Tabs/Tabs.utils.ts @@ -31,7 +31,7 @@ export function useTableEditorTabsCleanUp() { tabsFilteredToSchemas.push( ...openTabsRef.current.filter((tabId) => { const tab = tabMapRef.current[tabId] - return tab.metadata?.schema === schema + return tab?.metadata?.schema === schema }) ) } diff --git a/apps/studio/components/to-be-cleaned/Table.tsx b/apps/studio/components/to-be-cleaned/Table.tsx index c8d9077505a..17c0aa1db6f 100644 --- a/apps/studio/components/to-be-cleaned/Table.tsx +++ b/apps/studio/components/to-be-cleaned/Table.tsx @@ -11,6 +11,9 @@ interface TableProps { style?: React.StyleHTMLAttributes } +/** + * @deprecated Use `Table` from `ui` instead + */ function Table({ body, head, @@ -45,6 +48,9 @@ interface ThProps { style?: React.CSSProperties } +/** + * @deprecated Use `TableHeader` from `ui` instead + */ const Th = ({ children, className, style }: PropsWithChildren) => { const classes = ['p-3 px-4 text-left'] if (className) classes.push(className) @@ -62,6 +68,9 @@ interface TrProps { onClick?: () => void } +/** + * @deprecated Use `TableRow` from `ui` instead + */ const Tr = ({ children, className, onClick, style, hoverable }: PropsWithChildren) => { let classes = [className] if (onClick || hoverable) classes.push('tr--link') @@ -79,6 +88,9 @@ interface TdProps extends React.HTMLProps { align?: 'left' | 'center' | 'right' | 'justify' | 'char' | undefined } +/** + * @deprecated Use `TableCell` from `ui` instead + */ const Td = ({ children, colSpan, className, style, ...rest }: PropsWithChildren) => { return ( diff --git a/apps/studio/components/ui/AIAssistantPanel/AIAssistant.tsx b/apps/studio/components/ui/AIAssistantPanel/AIAssistant.tsx index 4d4c0346ad1..ad98e71ee94 100644 --- a/apps/studio/components/ui/AIAssistantPanel/AIAssistant.tsx +++ b/apps/studio/components/ui/AIAssistantPanel/AIAssistant.tsx @@ -2,7 +2,7 @@ import type { UIMessage as MessageType } from '@ai-sdk/react' import { useChat } from '@ai-sdk/react' import { DefaultChatTransport } from 'ai' import { AnimatePresence, motion } from 'framer-motion' -import { ArrowDown, Eraser, Info, Settings, X } from 'lucide-react' +import { ArrowDown, Eraser, Info, Pencil, Settings, X } from 'lucide-react' import { useRouter } from 'next/router' import { memo, useCallback, useEffect, useMemo, useRef, useState } from 'react' @@ -18,13 +18,13 @@ import { useOrgAiOptInLevel } from 'hooks/misc/useOrgOptedIntoAi' import { useSelectedOrganizationQuery } from 'hooks/misc/useSelectedOrganization' import { useSelectedProjectQuery } from 'hooks/misc/useSelectedProject' import { useFlag } from 'hooks/ui/useFlag' +import { useHotKey } from 'hooks/ui/useHotKey' import { BASE_PATH, IS_PLATFORM } from 'lib/constants' -import { tryParseJson } from 'lib/helpers' import uuidv4 from 'lib/uuid' import type { AssistantMessageType } from 'state/ai-assistant-state' import { useAiAssistantStateSnapshot } from 'state/ai-assistant-state' import { useSqlEditorV2StateSnapshot } from 'state/sql-editor-v2' -import { AiIconAnimation, Button, cn } from 'ui' +import { AiIconAnimation, Button, cn, KeyboardShortcut } from 'ui' import { Admonition, GenericSkeletonLoader } from 'ui-patterns' import { ButtonTooltip } from '../ButtonTooltip' import { ErrorBoundary } from '../ErrorBoundary' @@ -42,6 +42,10 @@ const MemoizedMessage = memo( message, isLoading, onResults, + onEdit, + isAfterEditedMessage, + isBeingEdited, + onCancelEdit, }: { message: MessageType isLoading: boolean @@ -54,15 +58,22 @@ const MemoizedMessage = memo( resultId?: string results: any[] }) => void + onEdit: (id: string) => void + isAfterEditedMessage: boolean + isBeingEdited: boolean + onCancelEdit: () => void }) => { return ( ) } @@ -82,6 +93,8 @@ export const AIAssistant = ({ className }: AIAssistantProps) => { const { ref, id: entityId } = useParams() const searchParams = useSearchParamsShallow() + useHotKey(() => cancelEdit(), 'Escape') + const disablePrompts = useFlag('disableAssistantPrompts') const { snippets } = useSqlEditorV2StateSnapshot() const snap = useAiAssistantStateSnapshot() @@ -105,6 +118,7 @@ export const AIAssistant = ({ className }: AIAssistantProps) => { const [value, setValue] = useState(snap.initialInput || '') const [isConfirmOptInModalOpen, setIsConfirmOptInModalOpen] = useState(false) + const [editingMessageId, setEditingMessageId] = useState(null) const { data: check, isSuccess } = useCheckOpenAIKeyQuery() const isApiKeySet = IS_PLATFORM || !!check?.hasKey @@ -235,25 +249,74 @@ export const AIAssistant = ({ className }: AIAssistantProps) => { [snap] ) + const editMessage = useCallback( + (messageId: string) => { + const messageIndex = chatMessages.findIndex((msg) => msg.id === messageId) + if (messageIndex === -1) return + + // Target message + const messageToEdit = chatMessages[messageIndex] + + // Activate editing mode + setEditingMessageId(messageId) + const textContent = + messageToEdit.parts + ?.filter((part) => part.type === 'text') + .map((part) => part.text) + .join('') ?? '' + setValue(textContent) + + if (inputRef.current) { + inputRef.current.focus() + } + }, + [chatMessages, setValue] + ) + + const cancelEdit = useCallback(() => { + setEditingMessageId(null) + setValue('') + }, [setValue]) + const renderedMessages = useMemo( () => - chatMessages.map((message) => { + chatMessages.map((message, index) => { + const isBeingEdited = editingMessageId === message.id + const isAfterEditedMessage = editingMessageId + ? chatMessages.findIndex((m) => m.id === editingMessageId) < index + : false + return ( ) }), - [chatMessages, isChatLoading, updateMessage] + [chatMessages, isChatLoading, updateMessage, editMessage, editingMessageId, cancelEdit] ) const hasMessages = chatMessages.length > 0 const isShowingOnboarding = !hasMessages && isApiKeySet const sendMessageToAssistant = (finalContent: string) => { + if (editingMessageId) { + // Handling when the user is in edit mode + const messageIndex = chatMessages.findIndex((msg) => msg.id === editingMessageId) + if (messageIndex === -1) return + + snap.deleteMessagesAfter(editingMessageId, { includeSelf: true }) + const updatedMessages = chatMessages.slice(0, messageIndex) + setMessages(updatedMessages) + setEditingMessageId(null) + } + const payload = { role: 'user', createdAt: new Date(), @@ -289,6 +352,7 @@ export const AIAssistant = ({ className }: AIAssistantProps) => { snap.clearMessages() setMessages([]) lastUserMessageRef.current = null + setEditingMessageId(null) } // Update scroll behavior for new messages @@ -432,7 +496,7 @@ export const AIAssistant = ({ className }: AIAssistantProps) => { )}
{hasMessages ? ( -
+
{renderedMessages} {error && (
@@ -528,40 +592,79 @@ export const AIAssistant = ({ className }: AIAssistantProps) => {
- {!isSticky && ( - <> - -
- - - -
-
- + /> +
+ +
+ + )} + {!isSticky && !editingMessageId && ( + +
+ + + +
+
)} diff --git a/apps/studio/components/ui/AIAssistantPanel/AssistantChatForm.tsx b/apps/studio/components/ui/AIAssistantPanel/AssistantChatForm.tsx index 75bca4790a0..f06425388fb 100644 --- a/apps/studio/components/ui/AIAssistantPanel/AssistantChatForm.tsx +++ b/apps/studio/components/ui/AIAssistantPanel/AssistantChatForm.tsx @@ -1,12 +1,12 @@ import { ArrowUp, Loader2, Square } from 'lucide-react' -import React, { ChangeEvent, memo, useRef } from 'react' +import { ChangeEvent, FormEvent, forwardRef, KeyboardEvent, memo, useRef } from 'react' import { useBreakpoint } from 'common' import { ExpandingTextArea } from 'ui' import { cn } from 'ui/src/lib/utils' import { ButtonTooltip } from '../ButtonTooltip' import { type SqlSnippet } from './AIAssistant.types' -import { SnippetRow, getSnippetContent } from './SnippetRow' +import { getSnippetContent, SnippetRow } from './SnippetRow' export interface FormProps { /* The ref for the textarea, optional. Exposed for the CommandsPopover to attach events. */ @@ -43,7 +43,7 @@ export interface FormProps { className?: string } -const AssistantChatFormComponent = React.forwardRef( +const AssistantChatFormComponent = forwardRef( ( { loading = false, @@ -66,7 +66,7 @@ const AssistantChatFormComponent = React.forwardRef( const formRef = useRef(null) const isMobile = useBreakpoint('md') - const handleSubmit = (event?: React.FormEvent) => { + const handleSubmit = (event?: FormEvent) => { if (event) event.preventDefault() if (!value || loading) return @@ -81,7 +81,7 @@ const AssistantChatFormComponent = React.forwardRef( onSubmit(finalMessage) } - const handleKeyDown = (event: React.KeyboardEvent) => { + const handleKeyDown = (event: KeyboardEvent) => { if (event.key === 'Enter' && !event.shiftKey) { event.preventDefault() handleSubmit() diff --git a/apps/studio/components/ui/AIAssistantPanel/Message.tsx b/apps/studio/components/ui/AIAssistantPanel/Message.tsx index 7838c5ceda0..3a295e2d6ce 100644 --- a/apps/studio/components/ui/AIAssistantPanel/Message.tsx +++ b/apps/studio/components/ui/AIAssistantPanel/Message.tsx @@ -1,5 +1,5 @@ import { UIMessage as VercelMessage } from '@ai-sdk/react' -import { Loader2 } from 'lucide-react' +import { Loader2, Pencil } from 'lucide-react' import { createContext, PropsWithChildren, ReactNode, useMemo } from 'react' import ReactMarkdown from 'react-markdown' import { Components } from 'react-markdown/lib/ast-to-react' @@ -8,6 +8,7 @@ import remarkGfm from 'remark-gfm' import { ProfileImage } from 'components/ui/ProfileImage' import { useProfile } from 'lib/profile' import { cn, markdownComponents, WarningIcon } from 'ui' +import { ButtonTooltip } from '../ButtonTooltip' import { EdgeFunctionBlock } from '../EdgeFunctionBlock/EdgeFunctionBlock' import { DisplayBlockRenderer } from './DisplayBlockRenderer' import { @@ -50,6 +51,10 @@ interface MessageProps { resultId?: string results: any[] }) => void + onEdit: (id: string) => void + isAfterEditedMessage: boolean + isBeingEdited: boolean + onCancelEdit: () => void } export const Message = function Message({ @@ -60,6 +65,10 @@ export const Message = function Message({ action = null, variant = 'default', onResults, + onEdit, + isAfterEditedMessage = false, + isBeingEdited = false, + onCancelEdit, }: PropsWithChildren) { const { profile } = useProfile() const allMarkdownComponents: Partial = useMemo( @@ -95,16 +104,18 @@ export const Message = function Message({
{variant === 'warning' && } {action} -
+
{isUser && ( )} -
+
{shouldUsePartsRendering ? ( (() => { const shownLoadingTools = new Set() @@ -126,7 +137,8 @@ export const Message = function Message({ key={`${id}-part-${index}`} className={cn( 'prose prose-sm [&>div]:my-4 prose-h1:text-xl prose-h1:mt-6 prose-h3:no-underline prose-h3:text-base prose-h3:mb-4 prose-strong:font-medium prose-strong:text-foreground break-words [&>p:not(:last-child)]:!mb-2 [&>*>p:first-child]:!mt-0 [&>*>p:last-child]:!mb-0 [&>*>*>p:first-child]:!mt-0 [&>*>*>p:last-child]:!mb-0 [&>ol>li]:!pl-4', - isUser && 'text-foreground [&>p]:font-medium' + isUser && 'text-foreground [&>p]:font-medium', + isBeingEdited && 'animate-pulse' )} remarkPlugins={[remarkGfm]} components={allMarkdownComponents} @@ -145,7 +157,7 @@ export const Message = function Message({ return (
{`Calling display_query...`} @@ -178,7 +190,7 @@ export const Message = function Message({ return (
{`Calling display_edge_function...`} @@ -224,6 +236,28 @@ export const Message = function Message({ ) : ( Assistant is thinking... )} + + {/* Action button - only show for user messages on hover */} +
+ {message.role === 'user' && ( + } + onClick={isBeingEdited || isAfterEditedMessage ? onCancelEdit : () => onEdit(id)} + className="text-foreground-light hover:text-foreground p-1 rounded" + aria-label={ + isBeingEdited || isAfterEditedMessage ? 'Cancel editing' : 'Edit message' + } + tooltip={{ + content: { + side: 'bottom', + text: + isBeingEdited || isAfterEditedMessage ? 'Cancel editing' : 'Edit message', + }, + }} + /> + )} +
diff --git a/apps/studio/components/ui/AIEditor/index.tsx b/apps/studio/components/ui/AIEditor/index.tsx index eb34c04b180..b27d47061e2 100644 --- a/apps/studio/components/ui/AIEditor/index.tsx +++ b/apps/studio/components/ui/AIEditor/index.tsx @@ -1,5 +1,4 @@ import Editor, { DiffEditor, Monaco, OnMount } from '@monaco-editor/react' -import { useCompletion } from '@ai-sdk/react' import { AnimatePresence, motion } from 'framer-motion' import { Command } from 'lucide-react' import { editor as monacoEditor } from 'monaco-editor' @@ -71,26 +70,64 @@ const AIEditor = ({ }) const [promptInput, setPromptInput] = useState(initialPrompt || '') - const { - complete, - completion, - isLoading: isCompletionLoading, - setCompletion, - } = useCompletion({ - api: aiEndpoint || '', - body: aiMetadata, - onError: (error) => { - toast.error(`Failed to generate: ${error.message}`) + const [isCompletionLoading, setIsCompletionLoading] = useState(false) + + const complete = useCallback( + async ( + prompt: string, + options?: { + headers?: Record + body?: { completionMetadata?: any } + } + ) => { + try { + if (!aiEndpoint) throw new Error('AI endpoint is not configured') + setIsCompletionLoading(true) + + const response = await fetch(aiEndpoint, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + ...(options?.headers ?? {}), + }, + body: JSON.stringify({ + ...(aiMetadata ?? {}), + ...(options?.body ?? {}), + }), + }) + + if (!response.ok) { + const errorText = await response.text() + throw new Error(errorText || 'Failed to generate completion') + } + + const text: string = await response.json() + + const meta = options?.body?.completionMetadata ?? {} + const beforeSelection: string = meta.textBeforeCursor ?? '' + const afterSelection: string = meta.textAfterCursor ?? '' + const selection: string = meta.selection ?? '' + + const original = beforeSelection + selection + afterSelection + const modified = beforeSelection + text + afterSelection + + setDiffValue({ original, modified }) + setIsDiffMode(true) + } catch (error: any) { + toast.error(`Failed to generate: ${error?.message ?? 'Unknown error'}`) + } finally { + setIsCompletionLoading(false) + } }, - }) + [aiEndpoint, aiMetadata] + ) const handleReset = useCallback(() => { - setCompletion('') setIsDiffMode(false) setPromptState((prev) => ({ ...prev, isOpen: false })) setPromptInput('') editorRef.current?.focus() - }, [setCompletion]) + }, []) const handleAcceptDiff = useCallback(() => { if (diffValue.modified) { @@ -269,20 +306,6 @@ const AIEditor = ({ } }, [isDiffMode]) - useEffect(() => { - if (!completion) { - setIsDiffMode(false) - return - } - - const original = - promptState.beforeSelection + promptState.selection + promptState.afterSelection - const modified = promptState.beforeSelection + completion + promptState.afterSelection - - setDiffValue({ original, modified }) - setIsDiffMode(true) - }, [completion, promptState.beforeSelection, promptState.selection, promptState.afterSelection]) - useEffect(() => { const handleKeyboard = (event: KeyboardEvent) => { if (event.key === 'Escape') { diff --git a/apps/studio/components/ui/Forms/FormSection.tsx b/apps/studio/components/ui/Forms/FormSection.tsx index c5b9881ffe3..3622bb9b814 100644 --- a/apps/studio/components/ui/Forms/FormSection.tsx +++ b/apps/studio/components/ui/Forms/FormSection.tsx @@ -65,11 +65,13 @@ const Shimmer = () => ( const FormSectionContent = ({ children, loading = true, + loaders, fullWidth, className, }: { children: React.ReactNode | string loading?: boolean + loaders?: number fullWidth?: boolean className?: string }) => { @@ -81,7 +83,11 @@ const FormSectionContent = ({ ${className} `} > - {loading ? Children.map(children, () => ) : children} + {loading + ? !!loaders + ? new Array(loaders).fill(0).map((_, idx) => ) + : Children.map(children, (_, idx) => ) + : children}
) } diff --git a/apps/studio/components/ui/PartnerManagedResource.tsx b/apps/studio/components/ui/PartnerManagedResource.tsx index c5c28b3844c..2acd744ed9a 100644 --- a/apps/studio/components/ui/PartnerManagedResource.tsx +++ b/apps/studio/components/ui/PartnerManagedResource.tsx @@ -3,9 +3,10 @@ import { ExternalLink } from 'lucide-react' import { useVercelRedirectQuery } from 'data/integrations/vercel-redirect-query' import { Alert_Shadcn_, AlertTitle_Shadcn_, Button } from 'ui' import PartnerIcon from './PartnerIcon' +import { MANAGED_BY, ManagedBy } from 'lib/constants/infrastructure' interface PartnerManagedResourceProps { - partner: 'vercel-marketplace' | 'aws-marketplace' + partner: ManagedBy resource: string cta?: { installationId?: string @@ -15,11 +16,13 @@ interface PartnerManagedResourceProps { } export const PARTNER_TO_NAME = { - 'vercel-marketplace': 'Vercel Marketplace', - 'aws-marketplace': 'AWS Marketplace', + [MANAGED_BY.VERCEL_MARKETPLACE]: 'Vercel Marketplace', + [MANAGED_BY.AWS_MARKETPLACE]: 'AWS Marketplace', + [MANAGED_BY.SUPABASE]: 'Supabase', } as const function PartnerManagedResource({ partner, resource, cta }: PartnerManagedResourceProps) { + const isManagedBySupabase = partner === MANAGED_BY.SUPABASE const ctaEnabled = cta !== undefined const { data, isLoading, isError } = useVercelRedirectQuery( @@ -27,10 +30,12 @@ function PartnerManagedResource({ partner, resource, cta }: PartnerManagedResour installationId: cta?.installationId, }, { - enabled: ctaEnabled, + enabled: ctaEnabled && !isManagedBySupabase, } ) + if (isManagedBySupabase) return null + const ctaUrl = (data?.url ?? '') + (cta?.path ?? '') return ( diff --git a/apps/studio/components/ui/SchemaSelector.tsx b/apps/studio/components/ui/SchemaSelector.tsx index d11e97581f0..72bd031de5b 100644 --- a/apps/studio/components/ui/SchemaSelector.tsx +++ b/apps/studio/components/ui/SchemaSelector.tsx @@ -3,7 +3,7 @@ import { Check, ChevronsUpDown, Plus } from 'lucide-react' import { useState } from 'react' import { useSchemasQuery } from 'data/database/schemas-query' -import { useCheckPermissions } from 'hooks/misc/useCheckPermissions' +import { useAsyncCheckProjectPermissions } from 'hooks/misc/useCheckPermissions' import { useSelectedProjectQuery } from 'hooks/misc/useSelectedProject' import { AlertDescription_Shadcn_, @@ -50,7 +50,10 @@ const SchemaSelector = ({ portal = true, }: SchemaSelectorProps) => { const [open, setOpen] = useState(false) - const canCreateSchemas = useCheckPermissions(PermissionAction.TENANT_SQL_ADMIN_WRITE, 'schemas') + const { can: canCreateSchemas } = useAsyncCheckProjectPermissions( + PermissionAction.TENANT_SQL_ADMIN_WRITE, + 'schemas' + ) const { data: project } = useSelectedProjectQuery() const { diff --git a/apps/studio/data/analytics/org-daily-stats-query.ts b/apps/studio/data/analytics/org-daily-stats-query.ts index 56e50f998e8..6a6e24f1ade 100644 --- a/apps/studio/data/analytics/org-daily-stats-query.ts +++ b/apps/studio/data/analytics/org-daily-stats-query.ts @@ -19,6 +19,7 @@ export enum EgressType { // [Joshen] Get this from common package instead of API and dashboard having one copy each export enum PricingMetric { EGRESS = 'EGRESS', + CACHED_EGRESS = 'CACHED_EGRESS', DATABASE_SIZE = 'DATABASE_SIZE', STORAGE_SIZE = 'STORAGE_SIZE', DISK_SIZE_GB_HOURS_GP3 = 'DISK_SIZE_GB_HOURS_GP3', diff --git a/apps/studio/data/api-keys/temp-api-keys-query.ts b/apps/studio/data/api-keys/temp-api-keys-query.ts new file mode 100644 index 00000000000..5a15846d269 --- /dev/null +++ b/apps/studio/data/api-keys/temp-api-keys-query.ts @@ -0,0 +1,30 @@ +import { handleError, post } from 'data/fetchers' + +interface getTemporaryAPIKeyVariables { + projectRef?: string + /** In seconds, max: 3600 (an hour) */ + expiry?: number +} + +// [Joshen] This one specifically shouldn't need a useQuery hook since the expiry is meant to be short lived +// Used in storage explorer and realtime inspector. +export async function getTemporaryAPIKey( + { projectRef, expiry = 300 }: getTemporaryAPIKeyVariables, + signal?: AbortSignal +) { + if (!projectRef) throw new Error('projectRef is required') + + const { data, error } = await post('/platform/projects/{ref}/api-keys/temporary', { + params: { + path: { ref: projectRef }, + query: { + authorization_exp: expiry.toString(), + claims: JSON.stringify({ role: 'service_role' }), + }, + }, + signal, + }) + + if (error) handleError(error) + return data +} diff --git a/apps/studio/data/branches/branch-merge-mutation.ts b/apps/studio/data/branches/branch-merge-mutation.ts index bceacc38223..4d2fe1effc1 100644 --- a/apps/studio/data/branches/branch-merge-mutation.ts +++ b/apps/studio/data/branches/branch-merge-mutation.ts @@ -21,7 +21,7 @@ export async function mergeBranch({ migration_version, }: BranchMergeVariables) { // Step 1: Get the diff output from the branch - const diffContent = await getBranchDiff({ branchId: id, includedSchemas: 'public' }) + const diffContent = await getBranchDiff({ branchId: id }) let migrationCreated = false diff --git a/apps/studio/data/database-triggers/database-triggers-query.ts b/apps/studio/data/database-triggers/database-triggers-query.ts index faa78e23fc2..5b80cc9fc3e 100644 --- a/apps/studio/data/database-triggers/database-triggers-query.ts +++ b/apps/studio/data/database-triggers/database-triggers-query.ts @@ -1,8 +1,8 @@ +import { DEFAULT_PLATFORM_APPLICATION_NAME } from '@supabase/pg-meta/src/constants' import { useQuery, UseQueryOptions } from '@tanstack/react-query' import { get, handleError } from 'data/fetchers' import type { ResponseError } from 'types' import { databaseTriggerKeys } from './keys' -import { DEFAULT_PLATFORM_APPLICATION_NAME } from '@supabase/pg-meta/src/constants' export type DatabaseTriggersVariables = { projectRef?: string diff --git a/apps/studio/data/organizations/organization-create-mutation.ts b/apps/studio/data/organizations/organization-create-mutation.ts index 5baffba7b98..93024acbb3b 100644 --- a/apps/studio/data/organizations/organization-create-mutation.ts +++ b/apps/studio/data/organizations/organization-create-mutation.ts @@ -93,3 +93,82 @@ export const useOrganizationCreateMutation = ({ } ) } + +export type AwsManagedOrganizationCreateVariables = { + name: string + kind?: string + size?: string + buyerId: string +} + +export async function createAwsManagedOrganization({ + name, + kind, + size, + buyerId, +}: AwsManagedOrganizationCreateVariables) { + const { data, error } = await post('/platform/organizations/cloud-marketplace', { + body: { + name, + kind, + size, + buyer_id: buyerId, + }, + }) + + if (error) handleError(error) + return data +} + +type AwsManagedOrganizationCreateData = Awaited> + +export const useAwsManagedOrganizationCreateMutation = ({ + onSuccess, + onError, + ...options +}: Omit< + UseMutationOptions< + AwsManagedOrganizationCreateData, + ResponseError, + AwsManagedOrganizationCreateVariables + >, + 'mutationFn' +> = {}) => { + const queryClient = useQueryClient() + + return useMutation< + AwsManagedOrganizationCreateData, + ResponseError, + AwsManagedOrganizationCreateVariables + >((vars) => createAwsManagedOrganization(vars), { + async onSuccess(data, variables, context) { + if (data) { + // [Joshen] We're manually updating the query client here as the org's subscription is + // created async, and the invalidation will happen too quick where the GET organizations + // endpoint will error out with a 500 since the subscription isn't created yet. + queryClient.setQueriesData( + { + queryKey: organizationKeys.list(), + exact: true, + }, + (prev: any) => { + if (!prev) return prev + return [...prev, castOrganizationResponseToOrganization(data)] + } + ) + + await queryClient.invalidateQueries(permissionKeys.list()) + } + + await onSuccess?.(data, variables, context) + }, + async onError(data, variables, context) { + if (onError === undefined) { + toast.error(`Failed to create organization: ${data.message}`) + } else { + onError(data, variables, context) + } + }, + ...options, + }) +} diff --git a/apps/studio/data/organizations/organization-link-aws-marketplace-mutation.ts b/apps/studio/data/organizations/organization-link-aws-marketplace-mutation.ts new file mode 100644 index 00000000000..a8ac033a764 --- /dev/null +++ b/apps/studio/data/organizations/organization-link-aws-marketplace-mutation.ts @@ -0,0 +1,51 @@ +import { useMutation, UseMutationOptions } from '@tanstack/react-query' +import { toast } from 'sonner' + +import { handleError, put } from '../fetchers' +import type { ResponseError } from '../../types' + +export type OrganizationLinkAwsMarketplaceVariables = { + buyerId: string + slug: string +} + +export async function linkOrganization({ buyerId, slug }: OrganizationLinkAwsMarketplaceVariables) { + const { data, error } = await put(`/platform/organizations/{slug}/cloud-marketplace/link`, { + params: { path: { slug } }, + body: { + buyer_id: buyerId, + }, + }) + + if (error) handleError(error) + + return data +} + +type LinkOrganizationData = Awaited> + +export const useOrganizationLinkAwsMarketplaceMutation = ({ + onSuccess, + onError, + ...options +}: Omit< + UseMutationOptions, + 'mutationFn' +> = {}) => { + return useMutation( + (vars) => linkOrganization(vars), + { + async onSuccess(data, variables, context) { + await onSuccess?.(data, variables, context) + }, + async onError(data, variables, context) { + if (onError === undefined) { + toast.error(`Failed to link organization to AWS Marketplace: ${data.message}`) + } else { + onError(data, variables, context) + } + }, + ...options, + } + ) +} diff --git a/apps/studio/data/organizations/organizations-query.ts b/apps/studio/data/organizations/organizations-query.ts index 1e6b8d1c35d..3ddbac115d3 100644 --- a/apps/studio/data/organizations/organizations-query.ts +++ b/apps/studio/data/organizations/organizations-query.ts @@ -5,6 +5,7 @@ import { get, handleError } from 'data/fetchers' import { useProfile } from 'lib/profile' import type { Organization, ResponseError } from 'types' import { organizationKeys } from './keys' +import { MANAGED_BY, ManagedBy } from 'lib/constants/infrastructure' export type OrganizationBase = components['schemas']['OrganizationResponse'] @@ -12,11 +13,25 @@ export function castOrganizationResponseToOrganization(org: OrganizationBase): O return { ...org, billing_email: org.billing_email ?? 'Unknown', - managed_by: org.slug.startsWith('vercel_icfg_') ? 'vercel-marketplace' : 'supabase', + managed_by: getManagedBy(org), partner_id: org.slug.startsWith('vercel_') ? org.slug.replace('vercel_', '') : undefined, } } +function getManagedBy(org: OrganizationBase): ManagedBy { + switch (org.billing_partner) { + case 'vercel_marketplace': + return MANAGED_BY.VERCEL_MARKETPLACE + // TODO(ignacio): Uncomment this when we've deployed the AWS Marketplace new slug + // case 'aws_marketplace': + // return MANAGED_BY.AWS_MARKETPLACE + case 'aws': + return MANAGED_BY.AWS_MARKETPLACE + default: + return MANAGED_BY.SUPABASE + } +} + export async function getOrganizations({ signal, headers, diff --git a/apps/studio/data/permissions/permissions-query.ts b/apps/studio/data/permissions/permissions-query.ts index 5de290221cc..975786dd73b 100644 --- a/apps/studio/data/permissions/permissions-query.ts +++ b/apps/studio/data/permissions/permissions-query.ts @@ -31,7 +31,7 @@ export const usePermissionsQuery = ({ { ...options, enabled: IS_PLATFORM && enabled && isLoggedIn, - staleTime: 30 * 60 * 1000, + staleTime: 5 * 60 * 1000, } ) } diff --git a/apps/studio/data/reports/database-charts.ts b/apps/studio/data/reports/database-charts.ts index 0efc542aedd..2f4eb36e21d 100644 --- a/apps/studio/data/reports/database-charts.ts +++ b/apps/studio/data/reports/database-charts.ts @@ -3,8 +3,17 @@ import { ReportAttributes } from 'components/ui/Charts/ComposedChart.utils' import { formatBytes } from 'lib/helpers' import { Organization } from 'types' import { Project } from '../projects/project-detail-query' +import { DiskAttributesData } from '../config/disk-attributes-query' +import { MaxConnectionsData } from '../database/max-connections-query' +import { PgbouncerConfigData } from '../database/pgbouncer-config-query' -export const getReportAttributes = (org: Organization, project: Project): ReportAttributes[] => { +export const getReportAttributes = ( + org: Organization, + project: Project, + diskConfig?: DiskAttributesData, + maxConnections?: MaxConnectionsData, + poolerConfig?: PgbouncerConfigData +): ReportAttributes[] => { const computeSize = project?.infra_compute_size || 'medium' return [ @@ -117,7 +126,7 @@ export const getReportAttributes = (org: Organization, project: Project): Report attribute: 'disk_iops_max', provider: 'reference-line', label: 'Max IOPS', - value: getIOPSLimits(computeSize), + value: diskConfig?.attributes?.iops, tooltip: 'Maximum IOPS (Input/Output Operations Per Second) for your current compute size', isMaxValue: true, @@ -210,10 +219,13 @@ export const getReportAttributes = (org: Organization, project: Project): Report ] } -export const getReportAttributesV2: (org: Organization, project: Project) => ReportAttributes[] = ( - org, - project -) => { +export const getReportAttributesV2: ( + org: Organization, + project: Project, + diskConfig?: DiskAttributesData, + maxConnections?: MaxConnectionsData, + poolerConfig?: PgbouncerConfigData +) => ReportAttributes[] = (org, project, diskConfig, maxConnections, poolerConfig) => { const isFreePlan = org?.plan?.id === 'free' const computeSize = project?.infra_compute_size || 'medium' const isSpendCapEnabled = @@ -369,7 +381,7 @@ export const getReportAttributesV2: (org: Organization, project: Project) => Rep attribute: 'disk_iops_max', provider: 'reference-line', label: 'Max IOPS', - value: getIOPSLimits(computeSize), + value: diskConfig?.attributes?.iops, tooltip: 'Maximum IOPS (Input/Output Operations Per Second) for your current compute size', isMaxValue: true, @@ -463,7 +475,7 @@ export const getReportAttributesV2: (org: Organization, project: Project) => Rep attribute: 'max_db_connections', provider: 'reference-line', label: 'Max connections', - value: getConnectionLimits(computeSize).direct, + value: maxConnections?.maxConnections, tooltip: 'Max available connections for your current compute size', isMaxValue: true, }, @@ -495,7 +507,7 @@ export const getReportAttributesV2: (org: Organization, project: Project) => Rep attribute: 'pg_pooler_max_connections', provider: 'reference-line', label: 'Max pooler connections', - value: getConnectionLimits(computeSize).pooler, + value: poolerConfig?.max_client_conn, tooltip: 'Maximum allowed pooler connections for your current compute size', isMaxValue: true, }, @@ -581,8 +593,7 @@ export const getReportAttributesV2: (org: Organization, project: Project) => Rep isReferenceLine: true, strokeDasharray: '4 2', label: 'Spend cap enabled', - value: - (project?.volumeSizeGb || getRecommendedDbSize(computeSize)) * 1024 * 1024 * 1024, + value: diskConfig?.attributes?.size_gb! * 1024 * 1024 * 1024, className: '[&_line]:!stroke-yellow-800 [&_line]:!opacity-100', opacity: 1, } @@ -592,77 +603,9 @@ export const getReportAttributesV2: (org: Organization, project: Project) => Rep isReferenceLine: true, label: '90% - Disk resize threshold', className: '[&_line]:!stroke-yellow-800', - value: - (project?.volumeSizeGb || getRecommendedDbSize(computeSize)) * - 1024 * - 1024 * - 1024 * - 0.9, // reaching 90% of the disk size will trigger a disk resize https://supabase.com/docs/guides/platform/database-size + value: diskConfig?.attributes?.size_gb! * 1024 * 1024 * 1024 * 0.9, }), ], }, ] } - -// Helper function to get connection limits based on compute size -export const getConnectionLimits = (computeSize: string = 'medium') => { - const connectionLimits = { - nano: { direct: 60, pooler: 200 }, - micro: { direct: 60, pooler: 200 }, - small: { direct: 90, pooler: 400 }, - medium: { direct: 120, pooler: 600 }, - large: { direct: 160, pooler: 800 }, - xlarge: { direct: 240, pooler: 1000 }, - '2xlarge': { direct: 380, pooler: 1500 }, - '4xlarge': { direct: 480, pooler: 3000 }, - '8xlarge': { direct: 490, pooler: 6000 }, - '12xlarge': { direct: 500, pooler: 9000 }, - '16xlarge': { direct: 500, pooler: 12000 }, - } - - return ( - connectionLimits[computeSize?.toLowerCase() as keyof typeof connectionLimits] || - connectionLimits.medium - ) -} - -// Helper function to get IOPS limits based on compute size -export const getIOPSLimits = (computeSize: string = 'medium') => { - const iopsLimits = { - nano: 250, - micro: 500, - small: 1000, - medium: 2000, - large: 3600, - xl: 6000, - '2xl': 12000, - '4xl': 20000, - '8xl': 40000, - '12xl': 50000, - '16xl': 80000, - } - - return iopsLimits[computeSize?.toLowerCase() as keyof typeof iopsLimits] || iopsLimits.medium -} - -// Helper function to get recommended DB size based on compute size (in GB) -export const getRecommendedDbSize = (computeSize: string = 'medium') => { - const recommendedSizes = { - nano: 0.5, // 500 MB - micro: 10, - small: 50, - medium: 100, - large: 200, - xl: 500, - '2xl': 1024, // 1 TB - '4xl': 2048, // 2 TB - '8xl': 4096, // 4 TB - '12xl': 6144, // 6 TB - '16xl': 10240, // 10 TB - } - - return ( - recommendedSizes[computeSize?.toLowerCase() as keyof typeof recommendedSizes] || - recommendedSizes.medium - ) -} diff --git a/apps/studio/data/tables/tables-query.ts b/apps/studio/data/tables/tables-query.ts index b7da827a5bb..af715ea5b60 100644 --- a/apps/studio/data/tables/tables-query.ts +++ b/apps/studio/data/tables/tables-query.ts @@ -3,10 +3,10 @@ import { useQuery, useQueryClient, type UseQueryOptions } from '@tanstack/react- import { sortBy } from 'lodash' import { useCallback } from 'react' +import { DEFAULT_PLATFORM_APPLICATION_NAME } from '@supabase/pg-meta/src/constants' import { get, handleError } from 'data/fetchers' import type { ResponseError } from 'types' import { tableKeys } from './keys' -import { DEFAULT_PLATFORM_APPLICATION_NAME } from '@supabase/pg-meta/src/constants' export type TablesVariables = { projectRef?: string diff --git a/apps/studio/hooks/misc/useChanged.ts b/apps/studio/hooks/misc/useChanged.ts new file mode 100644 index 00000000000..afeeb0f3198 --- /dev/null +++ b/apps/studio/hooks/misc/useChanged.ts @@ -0,0 +1,12 @@ +import { useEffect, useRef } from 'react' + +export function useChanged(value: T): boolean { + const prev = useRef() + const changed = prev.current !== value + + useEffect(() => { + prev.current = value + }) + + return changed +} diff --git a/apps/studio/hooks/ui/useCsvFileDrop.ts b/apps/studio/hooks/ui/useCsvFileDrop.ts new file mode 100644 index 00000000000..da2e91d449d --- /dev/null +++ b/apps/studio/hooks/ui/useCsvFileDrop.ts @@ -0,0 +1,61 @@ +import { type DragEvent, useCallback, useState } from 'react' + +import { type ImportDataFileDroppedEvent } from 'common/telemetry-constants' +import { flagInvalidFileImport } from 'components/interfaces/TableGridEditor/SidePanelEditor/SpreadsheetImport/SpreadsheetImport.utils' + +interface UseCsvFileDropOptions { + enabled: boolean + onFileDropped: (file: File) => void + onTelemetryEvent?: (eventName: ImportDataFileDroppedEvent['action']) => void +} + +interface UseCsvFileDropReturn { + isDraggedOver: boolean + onDragOver: (event: DragEvent) => void + onFileDrop: (event: DragEvent) => void +} + +export function useCsvFileDrop({ + enabled, + onFileDropped, + onTelemetryEvent, +}: UseCsvFileDropOptions): UseCsvFileDropReturn { + const [isDraggedOver, setIsDraggedOver] = useState(false) + + const onDragOver = useCallback( + (event: DragEvent) => { + if (!enabled) return + + if (event.type === 'dragover' && !isDraggedOver) { + setIsDraggedOver(true) + } else if (event.type === 'dragleave' || event.type === 'drop') { + setIsDraggedOver(false) + } + event.stopPropagation() + event.preventDefault() + }, + [enabled, isDraggedOver] + ) + + const onFileDrop = useCallback( + (event: DragEvent) => { + if (!enabled) return + + onDragOver(event) + + const [file] = event.dataTransfer.files + if (flagInvalidFileImport(file)) return + + onFileDropped(file) + + onTelemetryEvent?.('import_data_dropzone_file_added') + }, + [enabled, onDragOver, onFileDropped, onTelemetryEvent] + ) + + return { + isDraggedOver, + onDragOver, + onFileDrop, + } +} diff --git a/apps/studio/hooks/use-check-latest-deploy.tsx b/apps/studio/hooks/use-check-latest-deploy.tsx index cad2e77b005..eb09ccaeda3 100644 --- a/apps/studio/hooks/use-check-latest-deploy.tsx +++ b/apps/studio/hooks/use-check-latest-deploy.tsx @@ -42,7 +42,7 @@ export function useCheckLatestDeploy() { const { data: commit } = useDeploymentCommitQuery({ enabled: IS_PLATFORM && showRefreshToast, - staleTime: 10000, // 10 seconds + staleTime: 1000 * 60 * 10, // 10 minutes }) useEffect(() => { diff --git a/apps/studio/sentry.client.config.ts b/apps/studio/instrumentation-client.ts similarity index 95% rename from apps/studio/sentry.client.config.ts rename to apps/studio/instrumentation-client.ts index b1268e0a534..06eca90031d 100644 --- a/apps/studio/sentry.client.config.ts +++ b/apps/studio/instrumentation-client.ts @@ -63,8 +63,12 @@ Sentry.init({ const isInvalidUrlEvent = (hint.originalException as any)?.message?.includes( `Failed to construct 'URL': Invalid URL` ) + // [Joshen] Similar behaviour for this error from SessionTimeoutModal to control the quota usage + const isSessionTimeoutEvent = (hint.originalException as any)?.message.includes( + 'Session error detected' + ) - if (isInvalidUrlEvent && Math.random() > 0.01) { + if ((isInvalidUrlEvent || isSessionTimeoutEvent) && Math.random() > 0.01) { return null } diff --git a/apps/studio/lib/constants/infrastructure.ts b/apps/studio/lib/constants/infrastructure.ts index 78a76521753..4d2d4ffbf66 100644 --- a/apps/studio/lib/constants/infrastructure.ts +++ b/apps/studio/lib/constants/infrastructure.ts @@ -11,6 +11,14 @@ export const AWS_REGIONS_DEFAULT = // TO DO, change default to US region for prod export const FLY_REGIONS_DEFAULT = FLY_REGIONS.SOUTHEAST_ASIA +export const MANAGED_BY = { + VERCEL_MARKETPLACE: 'vercel-marketplace', + AWS_MARKETPLACE: 'aws-marketplace', + SUPABASE: 'supabase', +} + +export type ManagedBy = (typeof MANAGED_BY)[keyof typeof MANAGED_BY] + export const PRICING_TIER_LABELS_ORG = { FREE: 'Free - $0/month', PRO: 'Pro - $25/month', diff --git a/apps/studio/lib/constants/metrics.tsx b/apps/studio/lib/constants/metrics.tsx index 4ed2ed1a67c..5f17b6b8053 100644 --- a/apps/studio/lib/constants/metrics.tsx +++ b/apps/studio/lib/constants/metrics.tsx @@ -287,6 +287,14 @@ export const METRICS: Metric[] = [ provider: 'daily-stats', category: METRIC_CATEGORIES.API, }, + + { + key: 'total_cached_egress', + label: 'All Cached Egress', + provider: 'daily-stats', + category: METRIC_CATEGORIES.API_STORAGE, + }, + { key: 'total_storage_patch_requests', label: 'Storage PATCH Requests', diff --git a/apps/studio/lib/isNonNullable.test.ts b/apps/studio/lib/isNonNullable.test.ts new file mode 100644 index 00000000000..6c26736d19f --- /dev/null +++ b/apps/studio/lib/isNonNullable.test.ts @@ -0,0 +1,26 @@ +import { describe, it, expect } from 'vitest' +import { isNonNullable } from './isNonNullable.js' + +describe(`isNonNullable`, () => { + it.each([ + [null, false], + [undefined, false], + // void + [(() => {})(), false], + // Truthy + [`string`, true], + [1, true], + [true, true], + // Falsy + [``, true], + [NaN, true], + [0, true], + [0, true], + [false, true], + // Type coercion + [[], true], + [{}, true], + ])(`correctly matches against nullish values`, (val, expected) => { + expect(isNonNullable(val)).toStrictEqual(expected) + }) +}) diff --git a/apps/studio/lib/isNonNullable.ts b/apps/studio/lib/isNonNullable.ts new file mode 100644 index 00000000000..2fe7efdb6b5 --- /dev/null +++ b/apps/studio/lib/isNonNullable.ts @@ -0,0 +1,22 @@ +export type Maybe = T | null | undefined + +/** + * Used to test whether a `Maybe` typed value is `null` or `undefined`. + * + * When called, the given value's type is narrowed to `NonNullable`. + * + * ### Example Usage: + * + * ```ts + * const fn = (str: Maybe) => { + * if (!isNonNullable(str)) { + * // typeof str = null | undefined + * // ... + * } + * // typeof str = string + * // ... + * } + * ``` + */ +export const isNonNullable = >(val?: T): val is NonNullable => + typeof val !== `undefined` && val !== null diff --git a/apps/studio/package.json b/apps/studio/package.json index 9664e74ef14..d61993cf162 100644 --- a/apps/studio/package.json +++ b/apps/studio/package.json @@ -51,7 +51,7 @@ "@radix-ui/react-slider": "^1.1.2", "@radix-ui/react-slot": "^1.2.0", "@radix-ui/react-visually-hidden": "^1.1.3", - "@sentry/nextjs": "^8.52.1", + "@sentry/nextjs": "^10.3.0", "@std/path": "npm:@jsr/std__path@^1.0.8", "@stripe/react-stripe-js": "^3.7.0", "@stripe/stripe-js": "^7.5.0", diff --git a/apps/studio/pages/api/ai/code/complete.ts b/apps/studio/pages/api/ai/code/complete.ts index 157989ffdf5..13957ee1511 100644 --- a/apps/studio/pages/api/ai/code/complete.ts +++ b/apps/studio/pages/api/ai/code/complete.ts @@ -1,5 +1,5 @@ import pgMeta from '@supabase/pg-meta' -import { ModelMessage, stepCountIs, streamText } from 'ai' +import { ModelMessage, stepCountIs, generateText, Output } from 'ai' import { IS_PLATFORM } from 'common' import { source } from 'common-tags' import { executeSql } from 'data/sql/execute-sql-query' @@ -18,6 +18,7 @@ import { getTools } from 'lib/ai/tools' import apiWrapper from 'lib/api/apiWrapper' import { queryPgMetaSelfHosted } from 'lib/self-hosted' import { NextApiRequest, NextApiResponse } from 'next' +import { z } from 'zod/v4' export const maxDuration = 60 @@ -148,14 +149,19 @@ async function handler(req: NextApiRequest, res: NextApiResponse) { accessToken, }) - const result = streamText({ + const { experimental_output } = await generateText({ model, stopWhen: stepCountIs(5), + experimental_output: Output.object({ + schema: z.object({ + code: z.string().describe('The modified code'), + }), + }), messages: coreMessages, tools, }) - return result.pipeUIMessageStreamToResponse(res) + return res.status(200).json(experimental_output?.code) } catch (error) { console.error('Completion error:', error) return res.status(500).json({ diff --git a/apps/studio/pages/api/platform/organizations/[slug]/billing/subscription.ts b/apps/studio/pages/api/platform/organizations/[slug]/billing/subscription.ts index dfc1d786321..e5b946a8286 100644 --- a/apps/studio/pages/api/platform/organizations/[slug]/billing/subscription.ts +++ b/apps/studio/pages/api/platform/organizations/[slug]/billing/subscription.ts @@ -38,6 +38,7 @@ const handleGet = async (req: NextApiRequest, res: NextApiResponse billing_partner: 'fly', scheduled_plan_change: null, customer_balance: 0, + cached_egress_enabled: false, } return res.status(200).json(response) diff --git a/apps/studio/pages/aws-marketplace-onboarding.tsx b/apps/studio/pages/aws-marketplace-onboarding.tsx new file mode 100644 index 00000000000..0faea387ebf --- /dev/null +++ b/apps/studio/pages/aws-marketplace-onboarding.tsx @@ -0,0 +1,52 @@ +import { NextPageWithLayout } from '../types' +import { + ScaffoldContainer, + ScaffoldDivider, + ScaffoldHeader, + ScaffoldTitle, +} from '../components/layouts/Scaffold' +import LinkAwsMarketplaceLayout from '../components/layouts/LinkAwsMarketplaceLayout' +import { useOrganizationsQuery } from '../data/organizations/organizations-query' +import AwsMarketplaceLinkExistingOrg from '../components/interfaces/Organization/CloudMarketplace/AwsMarketplaceLinkExistingOrg' +import AwsMarketplaceCreateNewOrg from '../components/interfaces/Organization/CloudMarketplace/AwsMarketplaceCreateNewOrg' +import { useCloudMarketplaceOnboardingInfoQuery } from '../components/interfaces/Organization/CloudMarketplace/cloud-marketplace-query' +import { useRouter } from 'next/router' +import AwsMarketplaceOnboardingPlaceholder from '../components/interfaces/Organization/CloudMarketplace/AwsMarketplaceOnboardingPlaceholder' + +const AwsMarketplaceOnboarding: NextPageWithLayout = () => { + const { + query: { buyer_id: buyerId }, + } = useRouter() + + const { data: organizations, isFetched: isOrganizationsFetched } = useOrganizationsQuery() + const { data: onboardingInfo, isLoading: isLoadingOnboardingInfo } = + useCloudMarketplaceOnboardingInfoQuery({ + buyerId: buyerId as string, + }) + + return ( + + + AWS Marketplace Setup + + + {!isOrganizationsFetched ? ( + + ) : organizations?.length ? ( + + ) : ( + + )} + + ) +} + +AwsMarketplaceOnboarding.getLayout = (page) => ( + {page} +) + +export default AwsMarketplaceOnboarding diff --git a/apps/studio/pages/project/[ref]/branches/index.tsx b/apps/studio/pages/project/[ref]/branches/index.tsx index befbcd49271..006d7b5ff28 100644 --- a/apps/studio/pages/project/[ref]/branches/index.tsx +++ b/apps/studio/pages/project/[ref]/branches/index.tsx @@ -19,7 +19,7 @@ import { useBranchDeleteMutation } from 'data/branches/branch-delete-mutation' import { Branch, useBranchesQuery } from 'data/branches/branches-query' import { useGitHubConnectionsQuery } from 'data/integrations/github-connections-query' import { useSendEventMutation } from 'data/telemetry/send-event-mutation' -import { useCheckPermissions } from 'hooks/misc/useCheckPermissions' +import { useAsyncCheckProjectPermissions } from 'hooks/misc/useCheckPermissions' import { useSelectedOrganizationQuery } from 'hooks/misc/useSelectedOrganization' import { useSelectedProjectQuery } from 'hooks/misc/useSelectedProject' import { useAppStateSnapshot } from 'state/app-state' @@ -42,7 +42,10 @@ const BranchesPage: NextPageWithLayout = () => { const projectRef = project !== undefined ? (isBranch ? project.parent_project_ref : ref) : undefined - const canReadBranches = useCheckPermissions(PermissionAction.READ, 'preview_branches') + const { can: canReadBranches, isSuccess: isPermissionsLoaded } = useAsyncCheckProjectPermissions( + PermissionAction.READ, + 'preview_branches' + ) const { data: connections, @@ -121,7 +124,7 @@ const BranchesPage: NextPageWithLayout = () => {
- {!canReadBranches ? ( + {isPermissionsLoaded && !canReadBranches ? ( ) : ( <> @@ -183,9 +186,13 @@ const BranchesPage: NextPageWithLayout = () => { BranchesPage.getLayout = (page) => { const BranchesPageWrapper = () => { const snap = useAppStateSnapshot() - const canCreateBranches = useCheckPermissions(PermissionAction.CREATE, 'preview_branches', { - resource: { is_default: false }, - }) + const { can: canCreateBranches } = useAsyncCheckProjectPermissions( + PermissionAction.CREATE, + 'preview_branches', + { + resource: { is_default: false }, + } + ) const primaryActions = ( { const projectRef = project !== undefined ? (isBranch ? project.parent_project_ref : ref) : undefined - const canReadBranches = useCheckPermissions(PermissionAction.READ, 'preview_branches') + const { can: canReadBranches, isSuccess: isPermissionsLoaded } = useAsyncCheckProjectPermissions( + PermissionAction.READ, + 'preview_branches' + ) const { data: connections, @@ -163,7 +166,7 @@ const MergeRequestsPage: NextPageWithLayout = () => {
- {!canReadBranches ? ( + {isPermissionsLoaded && !canReadBranches ? ( ) : ( <> diff --git a/apps/studio/pages/project/[ref]/database/backups/pitr.tsx b/apps/studio/pages/project/[ref]/database/backups/pitr.tsx index b66eb6904d1..ae534f92f13 100644 --- a/apps/studio/pages/project/[ref]/database/backups/pitr.tsx +++ b/apps/studio/pages/project/[ref]/database/backups/pitr.tsx @@ -14,7 +14,7 @@ import NoPermission from 'components/ui/NoPermission' import { GenericSkeletonLoader } from 'components/ui/ShimmeringLoader' import UpgradeToPro from 'components/ui/UpgradeToPro' import { useBackupsQuery } from 'data/database/backups-query' -import { useCheckPermissions, usePermissionsLoaded } from 'hooks/misc/useCheckPermissions' +import { useAsyncCheckProjectPermissions } from 'hooks/misc/useCheckPermissions' import { useSelectedOrganizationQuery } from 'hooks/misc/useSelectedOrganization' import { useIsOrioleDbInAws, useSelectedProjectQuery } from 'hooks/misc/useSelectedProject' import { PROJECT_STATUS } from 'lib/constants' @@ -57,8 +57,8 @@ const PITR = () => { const isEnabled = backups?.pitr_enabled const isActiveHealthy = project?.status === PROJECT_STATUS.ACTIVE_HEALTHY - const isPermissionsLoaded = usePermissionsLoaded() - const canReadPhysicalBackups = useCheckPermissions(PermissionAction.READ, 'physical_backups') + const { can: canReadPhysicalBackups, isSuccess: isPermissionsLoaded } = + useAsyncCheckProjectPermissions(PermissionAction.READ, 'physical_backups') if (isPermissionsLoaded && !canReadPhysicalBackups) { return diff --git a/apps/studio/pages/project/[ref]/database/backups/restore-to-new-project.tsx b/apps/studio/pages/project/[ref]/database/backups/restore-to-new-project.tsx index 1e186f17cdd..31a58c8bccb 100644 --- a/apps/studio/pages/project/[ref]/database/backups/restore-to-new-project.tsx +++ b/apps/studio/pages/project/[ref]/database/backups/restore-to-new-project.tsx @@ -23,7 +23,7 @@ import UpgradeToPro from 'components/ui/UpgradeToPro' import { useDiskAttributesQuery } from 'data/config/disk-attributes-query' import { useCloneBackupsQuery } from 'data/projects/clone-query' import { useCloneStatusQuery } from 'data/projects/clone-status-query' -import { useCheckPermissions, usePermissionsLoaded } from 'hooks/misc/useCheckPermissions' +import { useAsyncCheckProjectPermissions } from 'hooks/misc/useCheckPermissions' import { useSelectedOrganizationQuery } from 'hooks/misc/useSelectedOrganization' import { useIsAwsK8sCloudProvider, @@ -80,12 +80,11 @@ const RestoreToNewProject = () => { isError, } = useCloneBackupsQuery({ projectRef: project?.ref }, { enabled: !isFreePlan }) - const plan = organization?.plan?.id const isActiveHealthy = project?.status === PROJECT_STATUS.ACTIVE_HEALTHY - const isPermissionsLoaded = usePermissionsLoaded() - const canReadPhysicalBackups = useCheckPermissions(PermissionAction.READ, 'physical_backups') - const canTriggerPhysicalBackups = useCheckPermissions( + const { can: canReadPhysicalBackups, isSuccess: isPermissionsLoaded } = + useAsyncCheckProjectPermissions(PermissionAction.READ, 'physical_backups') + const { can: canTriggerPhysicalBackups } = useAsyncCheckProjectPermissions( PermissionAction.INFRA_EXECUTE, 'queue_job.restore.prepare' ) diff --git a/apps/studio/pages/project/[ref]/database/backups/scheduled.tsx b/apps/studio/pages/project/[ref]/database/backups/scheduled.tsx index e9f40c21891..52b0838add5 100644 --- a/apps/studio/pages/project/[ref]/database/backups/scheduled.tsx +++ b/apps/studio/pages/project/[ref]/database/backups/scheduled.tsx @@ -14,7 +14,7 @@ import InformationBox from 'components/ui/InformationBox' import NoPermission from 'components/ui/NoPermission' import { GenericSkeletonLoader } from 'components/ui/ShimmeringLoader' import { useBackupsQuery } from 'data/database/backups-query' -import { useCheckPermissions, usePermissionsLoaded } from 'hooks/misc/useCheckPermissions' +import { useAsyncCheckProjectPermissions } from 'hooks/misc/useCheckPermissions' import { useIsOrioleDbInAws } from 'hooks/misc/useSelectedProject' import type { NextPageWithLayout } from 'types' import { Admonition } from 'ui-patterns' @@ -26,9 +26,9 @@ const DatabaseScheduledBackups: NextPageWithLayout = () => { const isOrioleDbInAws = useIsOrioleDbInAws() const isPitrEnabled = backups?.pitr_enabled - const isPermissionsLoaded = usePermissionsLoaded() - const canReadScheduledBackups = useCheckPermissions(PermissionAction.READ, 'back_ups') + const { can: canReadScheduledBackups, isSuccess: isPermissionsLoaded } = + useAsyncCheckProjectPermissions(PermissionAction.READ, 'back_ups') return ( diff --git a/apps/studio/pages/project/[ref]/database/extensions.tsx b/apps/studio/pages/project/[ref]/database/extensions.tsx index d1c7a3565f4..4239872f36a 100644 --- a/apps/studio/pages/project/[ref]/database/extensions.tsx +++ b/apps/studio/pages/project/[ref]/database/extensions.tsx @@ -4,31 +4,31 @@ import { Extensions } from 'components/interfaces/Database' import DatabaseLayout from 'components/layouts/DatabaseLayout/DatabaseLayout' import DefaultLayout from 'components/layouts/DefaultLayout' import { ScaffoldContainer, ScaffoldSection } from 'components/layouts/Scaffold' -import { FormHeader } from 'components/ui/Forms/FormHeader' +import { PageLayout } from 'components/layouts/PageLayout/PageLayout' import NoPermission from 'components/ui/NoPermission' -import { useCheckPermissions, usePermissionsLoaded } from 'hooks/misc/useCheckPermissions' +import { useAsyncCheckProjectPermissions } from 'hooks/misc/useCheckPermissions' import type { NextPageWithLayout } from 'types' const DatabaseExtensions: NextPageWithLayout = () => { - const canReadExtensions = useCheckPermissions( - PermissionAction.TENANT_SQL_ADMIN_READ, - 'extensions' - ) - const isPermissionsLoaded = usePermissionsLoaded() + const { can: canReadExtensions, isSuccess: isPermissionsLoaded } = + useAsyncCheckProjectPermissions(PermissionAction.TENANT_SQL_ADMIN_READ, 'extensions') if (isPermissionsLoaded && !canReadExtensions) { return } return ( - - -
- + + + -
-
-
+ +
+ ) } diff --git a/apps/studio/pages/project/[ref]/database/functions.tsx b/apps/studio/pages/project/[ref]/database/functions.tsx index c4653c0b56b..fc74e83002a 100644 --- a/apps/studio/pages/project/[ref]/database/functions.tsx +++ b/apps/studio/pages/project/[ref]/database/functions.tsx @@ -11,7 +11,7 @@ import { EditorPanel } from 'components/ui/EditorPanel/EditorPanel' import { FormHeader } from 'components/ui/Forms/FormHeader' import NoPermission from 'components/ui/NoPermission' import { DatabaseFunction } from 'data/database-functions/database-functions-query' -import { useCheckPermissions, usePermissionsLoaded } from 'hooks/misc/useCheckPermissions' +import { useAsyncCheckProjectPermissions } from 'hooks/misc/useCheckPermissions' import type { NextPageWithLayout } from 'types' const DatabaseFunctionsPage: NextPageWithLayout = () => { @@ -26,8 +26,10 @@ const DatabaseFunctionsPage: NextPageWithLayout = () => { DatabaseFunction | undefined >() - const canReadFunctions = useCheckPermissions(PermissionAction.TENANT_SQL_ADMIN_READ, 'functions') - const isPermissionsLoaded = usePermissionsLoaded() + const { can: canReadFunctions, isSuccess: isPermissionsLoaded } = useAsyncCheckProjectPermissions( + PermissionAction.TENANT_SQL_ADMIN_READ, + 'functions' + ) const createFunction = () => { if (isInlineEditorEnabled) { diff --git a/apps/studio/pages/project/[ref]/database/publications.tsx b/apps/studio/pages/project/[ref]/database/publications.tsx deleted file mode 100644 index 06b0c5bbffd..00000000000 --- a/apps/studio/pages/project/[ref]/database/publications.tsx +++ /dev/null @@ -1,66 +0,0 @@ -import { PermissionAction } from '@supabase/shared-types/out/constants' -import { useState } from 'react' - -import { PublicationsList, PublicationsTables } from 'components/interfaces/Database' -import DatabaseLayout from 'components/layouts/DatabaseLayout/DatabaseLayout' -import DefaultLayout from 'components/layouts/DefaultLayout' -import { ScaffoldContainer, ScaffoldSection } from 'components/layouts/Scaffold' -import { FormHeader } from 'components/ui/Forms/FormHeader' -import NoPermission from 'components/ui/NoPermission' -import { useDatabasePublicationsQuery } from 'data/database-publications/database-publications-query' -import { useCheckPermissions, usePermissionsLoaded } from 'hooks/misc/useCheckPermissions' -import { useSelectedProjectQuery } from 'hooks/misc/useSelectedProject' -import type { NextPageWithLayout } from 'types' - -// [Joshen] Technically, best that we have these as separate URLs -// makes it easier to manage state, but foresee that this page might -// be consolidated somewhere else eventually for better UX - -const DatabasePublications: NextPageWithLayout = () => { - const { data: project } = useSelectedProjectQuery() - - const { data } = useDatabasePublicationsQuery({ - projectRef: project?.ref, - connectionString: project?.connectionString, - }) - const publications = data ?? [] - - const canViewPublications = useCheckPermissions( - PermissionAction.TENANT_SQL_ADMIN_READ, - 'publications' - ) - const isPermissionsLoaded = usePermissionsLoaded() - - const [selectedPublicationId, setSelectedPublicationId] = useState() - const selectedPublication = publications.find((pub) => pub.id === selectedPublicationId) - - if (isPermissionsLoaded && !canViewPublications) { - return - } - - return ( - - -
- - {selectedPublication === undefined ? ( - - ) : ( - setSelectedPublicationId(undefined)} - /> - )} -
-
-
- ) -} - -DatabasePublications.getLayout = (page) => ( - - {page} - -) - -export default DatabasePublications diff --git a/apps/studio/pages/project/[ref]/database/publications/[id].tsx b/apps/studio/pages/project/[ref]/database/publications/[id].tsx new file mode 100644 index 00000000000..81fec4d623a --- /dev/null +++ b/apps/studio/pages/project/[ref]/database/publications/[id].tsx @@ -0,0 +1,37 @@ +import { PermissionAction } from '@supabase/shared-types/out/constants' + +import { PublicationsTables } from 'components/interfaces/Database/Publications/PublicationsTables' +import DatabaseLayout from 'components/layouts/DatabaseLayout/DatabaseLayout' +import DefaultLayout from 'components/layouts/DefaultLayout' +import { PageLayout } from 'components/layouts/PageLayout/PageLayout' +import { ScaffoldContainer, ScaffoldSection } from 'components/layouts/Scaffold' +import NoPermission from 'components/ui/NoPermission' +import { useAsyncCheckProjectPermissions } from 'hooks/misc/useCheckPermissions' +import type { NextPageWithLayout } from 'types' + +const DatabasePublications: NextPageWithLayout = () => { + const { can: canViewPublications, isSuccess: isPermissionsLoaded } = + useAsyncCheckProjectPermissions(PermissionAction.TENANT_SQL_ADMIN_READ, 'publications') + + if (isPermissionsLoaded && !canViewPublications) { + return + } + + return ( + + + + + + ) +} + +DatabasePublications.getLayout = (page) => ( + + + {page} + + +) + +export default DatabasePublications diff --git a/apps/studio/pages/project/[ref]/database/publications/index.tsx b/apps/studio/pages/project/[ref]/database/publications/index.tsx new file mode 100644 index 00000000000..b5ee05c33dc --- /dev/null +++ b/apps/studio/pages/project/[ref]/database/publications/index.tsx @@ -0,0 +1,37 @@ +import { PermissionAction } from '@supabase/shared-types/out/constants' + +import { PublicationsList } from 'components/interfaces/Database/Publications/PublicationsList' +import DatabaseLayout from 'components/layouts/DatabaseLayout/DatabaseLayout' +import DefaultLayout from 'components/layouts/DefaultLayout' +import { PageLayout } from 'components/layouts/PageLayout/PageLayout' +import { ScaffoldContainer, ScaffoldSection } from 'components/layouts/Scaffold' +import NoPermission from 'components/ui/NoPermission' +import { useAsyncCheckProjectPermissions } from 'hooks/misc/useCheckPermissions' +import type { NextPageWithLayout } from 'types' + +const DatabasePublications: NextPageWithLayout = () => { + const { can: canViewPublications, isSuccess: isPermissionsLoaded } = + useAsyncCheckProjectPermissions(PermissionAction.TENANT_SQL_ADMIN_READ, 'publications') + + if (isPermissionsLoaded && !canViewPublications) { + return + } + + return ( + + + + + + ) +} + +DatabasePublications.getLayout = (page) => ( + + + {page} + + +) + +export default DatabasePublications diff --git a/apps/studio/pages/project/[ref]/database/tables/[id].tsx b/apps/studio/pages/project/[ref]/database/tables/[id].tsx index 3a0cf30b1c2..4aece9c79a7 100644 --- a/apps/studio/pages/project/[ref]/database/tables/[id].tsx +++ b/apps/studio/pages/project/[ref]/database/tables/[id].tsx @@ -1,7 +1,7 @@ import { ChevronRight } from 'lucide-react' import { useParams } from 'common' -import { ColumnList } from 'components/interfaces/Database' +import { ColumnList } from 'components/interfaces/Database/Tables/ColumnList' import { SidePanelEditor } from 'components/interfaces/TableGridEditor' import DeleteConfirmationDialogs from 'components/interfaces/TableGridEditor/DeleteConfirmationDialogs' import DatabaseLayout from 'components/layouts/DatabaseLayout/DatabaseLayout' diff --git a/apps/studio/pages/project/[ref]/database/tables/index.tsx b/apps/studio/pages/project/[ref]/database/tables/index.tsx index 1b7075d19dc..9cd2e2d261d 100644 --- a/apps/studio/pages/project/[ref]/database/tables/index.tsx +++ b/apps/studio/pages/project/[ref]/database/tables/index.tsx @@ -1,8 +1,8 @@ -import { useState } from 'react' import { PostgresTable } from '@supabase/postgres-meta' +import { useState } from 'react' import { useParams } from 'common' -import { TableList } from 'components/interfaces/Database' +import { TableList } from 'components/interfaces/Database/Tables/TableList' import { SidePanelEditor } from 'components/interfaces/TableGridEditor' import DeleteConfirmationDialogs from 'components/interfaces/TableGridEditor/DeleteConfirmationDialogs' import DatabaseLayout from 'components/layouts/DatabaseLayout/DatabaseLayout' diff --git a/apps/studio/pages/project/[ref]/database/triggers.tsx b/apps/studio/pages/project/[ref]/database/triggers.tsx index 587f409b39d..4f90286f184 100644 --- a/apps/studio/pages/project/[ref]/database/triggers.tsx +++ b/apps/studio/pages/project/[ref]/database/triggers.tsx @@ -13,7 +13,7 @@ import { ScaffoldContainer, ScaffoldSection } from 'components/layouts/Scaffold' import { EditorPanel } from 'components/ui/EditorPanel/EditorPanel' import { FormHeader } from 'components/ui/Forms/FormHeader' import NoPermission from 'components/ui/NoPermission' -import { useCheckPermissions, usePermissionsLoaded } from 'hooks/misc/useCheckPermissions' +import { useAsyncCheckProjectPermissions } from 'hooks/misc/useCheckPermissions' import type { NextPageWithLayout } from 'types' const TriggersPage: NextPageWithLayout = () => { @@ -27,8 +27,10 @@ const TriggersPage: NextPageWithLayout = () => { const [editorPanelOpen, setEditorPanelOpen] = useState(false) const [selectedTriggerForEditor, setSelectedTriggerForEditor] = useState() - const canReadTriggers = useCheckPermissions(PermissionAction.TENANT_SQL_ADMIN_READ, 'triggers') - const isPermissionsLoaded = usePermissionsLoaded() + const { can: canReadTriggers, isSuccess: isPermissionsLoaded } = useAsyncCheckProjectPermissions( + PermissionAction.TENANT_SQL_ADMIN_READ, + 'triggers' + ) const createTrigger = () => { if (isInlineEditorEnabled) { diff --git a/apps/studio/pages/project/[ref]/functions/[functionSlug]/code.tsx b/apps/studio/pages/project/[ref]/functions/[functionSlug]/code.tsx index b444c84d4a3..be79cc42624 100644 --- a/apps/studio/pages/project/[ref]/functions/[functionSlug]/code.tsx +++ b/apps/studio/pages/project/[ref]/functions/[functionSlug]/code.tsx @@ -14,8 +14,7 @@ import { useEdgeFunctionBodyQuery } from 'data/edge-functions/edge-function-body import { useEdgeFunctionQuery } from 'data/edge-functions/edge-function-query' import { useEdgeFunctionDeployMutation } from 'data/edge-functions/edge-functions-deploy-mutation' import { useSendEventMutation } from 'data/telemetry/send-event-mutation' -import { useCheckPermissions } from 'hooks/misc/useCheckPermissions' -import { useOrgAiOptInLevel } from 'hooks/misc/useOrgOptedIntoAi' +import { useAsyncCheckProjectPermissions } from 'hooks/misc/useCheckPermissions' import { useSelectedOrganizationQuery } from 'hooks/misc/useSelectedOrganization' import { useSelectedProjectQuery } from 'hooks/misc/useSelectedProject' import { BASE_PATH } from 'lib/constants' @@ -29,7 +28,10 @@ const CodePage = () => { const { mutate: sendEvent } = useSendEventMutation() const [showDeployWarning, setShowDeployWarning] = useState(false) - const canDeployFunction = useCheckPermissions(PermissionAction.FUNCTIONS_WRITE, '*') + const { can: canDeployFunction } = useAsyncCheckProjectPermissions( + PermissionAction.FUNCTIONS_WRITE, + '*' + ) const { data: selectedFunction } = useEdgeFunctionQuery({ projectRef: ref, slug: functionSlug }) const { diff --git a/apps/studio/pages/project/[ref]/functions/index.tsx b/apps/studio/pages/project/[ref]/functions/index.tsx index c7e4b532550..7a76544467c 100644 --- a/apps/studio/pages/project/[ref]/functions/index.tsx +++ b/apps/studio/pages/project/[ref]/functions/index.tsx @@ -11,14 +11,13 @@ import DefaultLayout from 'components/layouts/DefaultLayout' import EdgeFunctionsLayout from 'components/layouts/EdgeFunctionsLayout/EdgeFunctionsLayout' import { PageLayout } from 'components/layouts/PageLayout/PageLayout' import { ScaffoldContainer, ScaffoldSection } from 'components/layouts/Scaffold' -import Table from 'components/to-be-cleaned/Table' import AlertError from 'components/ui/AlertError' import { DocsButton } from 'components/ui/DocsButton' import { GenericSkeletonLoader } from 'components/ui/ShimmeringLoader' import { useEdgeFunctionsQuery } from 'data/edge-functions/edge-functions-query' import { IS_PLATFORM } from 'lib/constants' import type { NextPageWithLayout } from 'types' -import { Button } from 'ui' +import { Button, Table, TableHead, TableRow, TableHeader, TableBody, Card } from 'ui' const EdgeFunctionsPage: NextPageWithLayout = () => { const { ref } = useParams() @@ -62,25 +61,28 @@ const EdgeFunctionsPage: NextPageWithLayout = () => { {isSuccess && ( <> {hasFunctions ? ( - - Name - URL - Created - Last updated - Deployments - - } - body={ - <> - {functions.length > 0 && - functions.map((item) => ( - - ))} - - } - /> + +
+ + + Name + URL + Created + Last updated + Deployments + + + + + <> + {functions.length > 0 && + functions.map((item) => ( + + ))} + + +
+ ) : ( )} diff --git a/apps/studio/pages/project/[ref]/functions/secrets.tsx b/apps/studio/pages/project/[ref]/functions/secrets.tsx index ebc487a7551..d9df3ec1e52 100644 --- a/apps/studio/pages/project/[ref]/functions/secrets.tsx +++ b/apps/studio/pages/project/[ref]/functions/secrets.tsx @@ -3,14 +3,16 @@ import { FunctionsSecretsEmptyStateLocal } from 'components/interfaces/Functions import DefaultLayout from 'components/layouts/DefaultLayout' import EdgeFunctionsLayout from 'components/layouts/EdgeFunctionsLayout/EdgeFunctionsLayout' import { PageLayout } from 'components/layouts/PageLayout/PageLayout' -import { ScaffoldContainer } from 'components/layouts/Scaffold' +import { ScaffoldContainer, ScaffoldSection } from 'components/layouts/Scaffold' import { IS_PLATFORM } from 'lib/constants' import type { NextPageWithLayout } from 'types' const SecretsPage: NextPageWithLayout = () => { return ( - - {IS_PLATFORM ? : } + + + {IS_PLATFORM ? : } + ) } diff --git a/apps/studio/pages/project/[ref]/index.tsx b/apps/studio/pages/project/[ref]/index.tsx index 5c0b6881ba1..1ca640ba44b 100644 --- a/apps/studio/pages/project/[ref]/index.tsx +++ b/apps/studio/pages/project/[ref]/index.tsx @@ -94,7 +94,7 @@ const Home: NextPageWithLayout = () => { return (
-
+
@@ -205,7 +205,7 @@ const Home: NextPageWithLayout = () => { {!isPaused && ( <> -
+
{IS_PLATFORM && project?.status !== PROJECT_STATUS.INACTIVE && ( <>{isNewProject ? : } diff --git a/apps/studio/pages/project/[ref]/logs/index.tsx b/apps/studio/pages/project/[ref]/logs/index.tsx index 16e56270226..2c04272a4c1 100644 --- a/apps/studio/pages/project/[ref]/logs/index.tsx +++ b/apps/studio/pages/project/[ref]/logs/index.tsx @@ -1,54 +1,25 @@ import { useRouter } from 'next/router' -import { useContext, useEffect } from 'react' +import { useEffect } from 'react' -import { FeatureFlagContext, LOCAL_STORAGE_KEYS, useParams } from 'common' +import { useParams } from 'common' import { useUnifiedLogsPreview } from 'components/interfaces/App/FeaturePreview/FeaturePreviewContext' import { UnifiedLogs } from 'components/interfaces/UnifiedLogs/UnifiedLogs' import DefaultLayout from 'components/layouts/DefaultLayout' -import LogsLayout from 'components/layouts/LogsLayout/LogsLayout' import ProjectLayout from 'components/layouts/ProjectLayout/ProjectLayout' -import { useLocalStorageQuery } from 'hooks/misc/useLocalStorage' -import { useSelectedOrganizationQuery } from 'hooks/misc/useSelectedOrganization' -import { IS_PLATFORM } from 'lib/constants' -import type { NextPageWithLayout } from 'types' +import { NextPageWithLayout } from 'types' export const LogPage: NextPageWithLayout = () => { const router = useRouter() const { ref } = useParams() - const { hasLoaded } = useContext(FeatureFlagContext) - const { data: org } = useSelectedOrganizationQuery() const { isEnabled: isUnifiedLogsEnabled } = useUnifiedLogsPreview() - const [lastVisitedLogsPage] = useLocalStorageQuery( - LOCAL_STORAGE_KEYS.LAST_VISITED_LOGS_PAGE, - 'explorer' - ) - useEffect(() => { - if (hasLoaded && !!org && !isUnifiedLogsEnabled) { - router.replace(`/project/${ref}/logs/${lastVisitedLogsPage}`) + if (!isUnifiedLogsEnabled && ref) { + router.replace(`/project/${ref}/logs/explorer`) } - }, [router, hasLoaded, org, lastVisitedLogsPage, ref, isUnifiedLogsEnabled]) - - // Handle redirects when unified logs preview flag changes - useEffect(() => { - // Only handle redirects if we're currently on a logs page - if (!router.asPath.includes('/logs') || (IS_PLATFORM && !hasLoaded)) return - - if (IS_PLATFORM && isUnifiedLogsEnabled) { - // If unified logs preview is enabled and we're not already on the main logs page - if (router.asPath !== `/project/${ref}/logs` && router.asPath.includes('/logs/')) { - router.push(`/project/${ref}/logs`) - } - } else { - // If unified logs preview is disabled and admin flag is also off - // and we're on the main logs page, redirect to explorer - if (router.asPath === `/project/${ref}/logs`) { - router.push(`/project/${ref}/logs/explorer`) - } - } - }, [isUnifiedLogsEnabled, router, ref, hasLoaded]) + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [isUnifiedLogsEnabled, ref]) if (isUnifiedLogsEnabled) { return ( @@ -60,14 +31,7 @@ export const LogPage: NextPageWithLayout = () => { ) } - return ( - - - {/* Empty placeholder - the useEffect will handle redirect */} -
-
-
- ) + return null } // Don't use getLayout since we're handling layouts conditionally within the component diff --git a/apps/studio/pages/project/[ref]/realtime/inspector.tsx b/apps/studio/pages/project/[ref]/realtime/inspector.tsx index 12c7968597f..5f8b7314cff 100644 --- a/apps/studio/pages/project/[ref]/realtime/inspector.tsx +++ b/apps/studio/pages/project/[ref]/realtime/inspector.tsx @@ -1,19 +1,10 @@ -import { PermissionAction } from '@supabase/shared-types/out/constants' import type { NextPageWithLayout } from 'types' import { RealtimeInspector } from 'components/interfaces/Realtime/Inspector' -import RealtimeLayout from 'components/layouts/RealtimeLayout/RealtimeLayout' import DefaultLayout from 'components/layouts/DefaultLayout' -import NoPermission from 'components/ui/NoPermission' -import { useCheckPermissions } from 'hooks/misc/useCheckPermissions' +import RealtimeLayout from 'components/layouts/RealtimeLayout/RealtimeLayout' export const InspectorPage: NextPageWithLayout = () => { - const canReadAPIKeys = useCheckPermissions(PermissionAction.READ, 'service_api_keys') - - if (!canReadAPIKeys) { - return - } - return } diff --git a/apps/studio/pages/project/[ref]/reports/[id].tsx b/apps/studio/pages/project/[ref]/reports/[id].tsx index 9f74349d499..b149431d222 100644 --- a/apps/studio/pages/project/[ref]/reports/[id].tsx +++ b/apps/studio/pages/project/[ref]/reports/[id].tsx @@ -5,7 +5,7 @@ import ReportsLayout from 'components/layouts/ReportsLayout/ReportsLayout' import type { NextPageWithLayout } from 'types' const PageLayout: NextPageWithLayout = () => ( -
+
diff --git a/apps/studio/pages/project/[ref]/reports/database.tsx b/apps/studio/pages/project/[ref]/reports/database.tsx index a598bb3712b..6a3fd20c334 100644 --- a/apps/studio/pages/project/[ref]/reports/database.tsx +++ b/apps/studio/pages/project/[ref]/reports/database.tsx @@ -1,16 +1,16 @@ import { PermissionAction } from '@supabase/shared-types/out/constants' import { useQueryClient } from '@tanstack/react-query' -import { useParams } from 'common' import dayjs from 'dayjs' import { ArrowRight, ExternalLink, RefreshCw } from 'lucide-react' import Link from 'next/link' import { useEffect, useState } from 'react' import { toast } from 'sonner' -import { AlertDescription_Shadcn_, Alert_Shadcn_, Button } from 'ui' +import { useParams } from 'common' import ReportChart from 'components/interfaces/Reports/ReportChart' import ReportHeader from 'components/interfaces/Reports/ReportHeader' import ReportPadding from 'components/interfaces/Reports/ReportPadding' +import { REPORT_DATERANGE_HELPER_LABELS } from 'components/interfaces/Reports/Reports.constants' import ReportStickyNav from 'components/interfaces/Reports/ReportStickyNav' import ReportWidget from 'components/interfaces/Reports/ReportWidget' import DiskSizeConfigurationModal from 'components/interfaces/Settings/Database/DiskSizeConfigurationModal' @@ -21,26 +21,27 @@ import ReportsLayout from 'components/layouts/ReportsLayout/ReportsLayout' import Table from 'components/to-be-cleaned/Table' import { ButtonTooltip } from 'components/ui/ButtonTooltip' import ChartHandler from 'components/ui/Charts/ChartHandler' +import type { MultiAttribute } from 'components/ui/Charts/ComposedChart.utils' import ComposedChartHandler from 'components/ui/Charts/ComposedChartHandler' import GrafanaPromoBanner from 'components/ui/GrafanaPromoBanner' import Panel from 'components/ui/Panel' -import { useDatabaseSelectorStateSnapshot } from 'state/database-selector' - -import { REPORT_DATERANGE_HELPER_LABELS } from 'components/interfaces/Reports/Reports.constants' import { analyticsKeys } from 'data/analytics/keys' import { useProjectDiskResizeMutation } from 'data/config/project-disk-resize-mutation' +import { useDiskAttributesQuery } from 'data/config/disk-attributes-query' import { useDatabaseSizeQuery } from 'data/database/database-size-query' +import { useMaxConnectionsQuery } from 'data/database/max-connections-query' +import { usePgbouncerConfigQuery } from 'data/database/pgbouncer-config-query' import { getReportAttributes, getReportAttributesV2 } from 'data/reports/database-charts' import { useDatabaseReport } from 'data/reports/database-report-query' -import { useCheckPermissions } from 'hooks/misc/useCheckPermissions' +import { useAsyncCheckProjectPermissions } from 'hooks/misc/useCheckPermissions' import { useReportDateRange } from 'hooks/misc/useReportDateRange' import { useSelectedOrganizationQuery } from 'hooks/misc/useSelectedOrganization' +import { useSelectedProjectQuery } from 'hooks/misc/useSelectedProject' import { useFlag } from 'hooks/ui/useFlag' import { formatBytes } from 'lib/helpers' - -import type { MultiAttribute } from 'components/ui/Charts/ComposedChart.utils' -import { useSelectedProjectQuery } from 'hooks/misc/useSelectedProject' +import { useDatabaseSelectorStateSnapshot } from 'state/database-selector' import type { NextPageWithLayout } from 'types' +import { AlertDescription_Shadcn_, Alert_Shadcn_, Button } from 'ui' const DatabaseReport: NextPageWithLayout = () => { return ( @@ -99,14 +100,37 @@ const DatabaseUsage = () => { const databaseSizeBytes = databaseSizeData ?? 0 const currentDiskSize = project?.volumeSizeGb ?? 0 - const canUpdateDiskSizeConfig = useCheckPermissions(PermissionAction.UPDATE, 'projects', { - resource: { - project_id: project?.id, - }, + const { data: diskConfig } = useDiskAttributesQuery({ projectRef: project?.ref }) + const { data: maxConnections } = useMaxConnectionsQuery({ + projectRef: project?.ref, + connectionString: project?.connectionString, }) + const { data: poolerConfig } = usePgbouncerConfigQuery({ projectRef: project?.ref }) - const REPORT_ATTRIBUTES = getReportAttributes(org!, project!) - const REPORT_ATTRIBUTES_V2 = getReportAttributesV2(org!, project!) + const { can: canUpdateDiskSizeConfig } = useAsyncCheckProjectPermissions( + PermissionAction.UPDATE, + 'projects', + { + resource: { + project_id: project?.id, + }, + } + ) + + const REPORT_ATTRIBUTES = getReportAttributes( + org!, + project!, + diskConfig, + maxConnections, + poolerConfig + ) + const REPORT_ATTRIBUTES_V2 = getReportAttributesV2( + org!, + project!, + diskConfig, + maxConnections, + poolerConfig + ) const { isLoading: isUpdatingDiskSize } = useProjectDiskResizeMutation({ onSuccess: (_, variables) => { diff --git a/apps/studio/pages/project/[ref]/reports/index.tsx b/apps/studio/pages/project/[ref]/reports/index.tsx index c063658e31f..ff2cc6e12e6 100644 --- a/apps/studio/pages/project/[ref]/reports/index.tsx +++ b/apps/studio/pages/project/[ref]/reports/index.tsx @@ -4,14 +4,14 @@ import { useState } from 'react' import { useParams } from 'common' import { CreateReportModal } from 'components/interfaces/Reports/CreateReportModal' +import DefaultLayout from 'components/layouts/DefaultLayout' import ReportsLayout from 'components/layouts/ReportsLayout/ReportsLayout' import ProductEmptyState from 'components/to-be-cleaned/ProductEmptyState' import { Loading } from 'components/ui/Loading' import { useContentQuery } from 'data/content/content-query' -import { useCheckPermissions } from 'hooks/misc/useCheckPermissions' +import { useAsyncCheckProjectPermissions } from 'hooks/misc/useCheckPermissions' import { useProfile } from 'lib/profile' import type { NextPageWithLayout } from 'types' -import DefaultLayout from 'components/layouts/DefaultLayout' export const UserReportPage: NextPageWithLayout = () => { const router = useRouter() @@ -36,10 +36,14 @@ export const UserReportPage: NextPageWithLayout = () => { } ) - const canCreateReport = useCheckPermissions(PermissionAction.CREATE, 'user_content', { - resource: { type: 'report', owner_id: profile?.id }, - subject: { id: profile?.id }, - }) + const { can: canCreateReport } = useAsyncCheckProjectPermissions( + PermissionAction.CREATE, + 'user_content', + { + resource: { type: 'report', owner_id: profile?.id }, + subject: { id: profile?.id }, + } + ) return (
diff --git a/apps/studio/pages/project/[ref]/settings/jwt/signing-keys.tsx b/apps/studio/pages/project/[ref]/settings/jwt/signing-keys.tsx index 406f8d9ae01..72f40dd8e68 100644 --- a/apps/studio/pages/project/[ref]/settings/jwt/signing-keys.tsx +++ b/apps/studio/pages/project/[ref]/settings/jwt/signing-keys.tsx @@ -1,16 +1,19 @@ import { PermissionAction } from '@supabase/shared-types/out/constants' + import JWTSecretKeysTable from 'components/interfaces/JwtSecrets/jwt-secret-keys-table' import DefaultLayout from 'components/layouts/DefaultLayout' import JWTKeysLayout from 'components/layouts/JWTKeys/JWTKeysLayout' import SettingsLayout from 'components/layouts/ProjectSettingsLayout/SettingsLayout' import NoPermission from 'components/ui/NoPermission' import { GenericSkeletonLoader } from 'components/ui/ShimmeringLoader' -import { useCheckPermissions, usePermissionsLoaded } from 'hooks/misc/useCheckPermissions' +import { useAsyncCheckProjectPermissions } from 'hooks/misc/useCheckPermissions' import type { NextPageWithLayout } from 'types' const JWTSigningKeysPage: NextPageWithLayout = () => { - const isPermissionsLoaded = usePermissionsLoaded() - const canReadAPIKeys = useCheckPermissions(PermissionAction.READ, 'auth_signing_keys') + const { can: canReadAPIKeys, isSuccess: isPermissionsLoaded } = useAsyncCheckProjectPermissions( + PermissionAction.READ, + 'auth_signing_keys' + ) return ( <> diff --git a/apps/studio/pages/project/[ref]/settings/log-drains.tsx b/apps/studio/pages/project/[ref]/settings/log-drains.tsx index e84dcaea91f..cdd982e072b 100644 --- a/apps/studio/pages/project/[ref]/settings/log-drains.tsx +++ b/apps/studio/pages/project/[ref]/settings/log-drains.tsx @@ -6,6 +6,7 @@ import { useParams } from 'common' import { LogDrainDestinationSheetForm } from 'components/interfaces/LogDrains/LogDrainDestinationSheetForm' import { LogDrains } from 'components/interfaces/LogDrains/LogDrains' import { LogDrainType } from 'components/interfaces/LogDrains/LogDrains.constants' +import DefaultLayout from 'components/layouts/DefaultLayout' import SettingsLayout from 'components/layouts/ProjectSettingsLayout/SettingsLayout' import { ScaffoldContainer, @@ -17,14 +18,15 @@ import { DocsButton } from 'components/ui/DocsButton' import { useCreateLogDrainMutation } from 'data/log-drains/create-log-drain-mutation' import { LogDrainData, useLogDrainsQuery } from 'data/log-drains/log-drains-query' import { useUpdateLogDrainMutation } from 'data/log-drains/update-log-drain-mutation' -import { useCheckPermissions } from 'hooks/misc/useCheckPermissions' +import { useAsyncCheckProjectPermissions } from 'hooks/misc/useCheckPermissions' import { useCurrentOrgPlan } from 'hooks/misc/useCurrentOrgPlan' import type { NextPageWithLayout } from 'types' import { Alert_Shadcn_, Button } from 'ui' -import DefaultLayout from 'components/layouts/DefaultLayout' +import { GenericSkeletonLoader } from 'ui-patterns' const LogDrainsSettings: NextPageWithLayout = () => { - const canManageLogDrains = useCheckPermissions(PermissionAction.ANALYTICS_ADMIN_WRITE, 'logflare') + const { can: canManageLogDrains, isLoading: isLoadingPermissions } = + useAsyncCheckProjectPermissions(PermissionAction.ANALYTICS_ADMIN_WRITE, 'logflare') const [open, setOpen] = useState(false) const { ref } = useParams() as { ref: string } @@ -35,12 +37,7 @@ const LogDrainsSettings: NextPageWithLayout = () => { const logDrainsEnabled = !planLoading && (plan?.id === 'team' || plan?.id === 'enterprise') - const { data: logDrains } = useLogDrainsQuery( - { ref }, - { - enabled: logDrainsEnabled, - } - ) + const { data: logDrains } = useLogDrainsQuery({ ref }, { enabled: logDrainsEnabled }) const { mutate: createLogDrain, isLoading: createLoading } = useCreateLogDrainMutation({ onSuccess: () => { @@ -144,12 +141,15 @@ const LogDrainsSettings: NextPageWithLayout = () => { } }} /> - {canManageLogDrains ? ( - - ) : ( + + {isLoadingPermissions ? ( + + ) : !canManageLogDrains ? ( You do not have permission to manage log drains + ) : ( + )} diff --git a/apps/studio/pages/project/[ref]/storage/buckets/[bucketId].tsx b/apps/studio/pages/project/[ref]/storage/buckets/[bucketId].tsx index 4b432682bf8..f55a43c03ba 100644 --- a/apps/studio/pages/project/[ref]/storage/buckets/[bucketId].tsx +++ b/apps/studio/pages/project/[ref]/storage/buckets/[bucketId].tsx @@ -20,7 +20,7 @@ const PageLayout: NextPageWithLayout = () => { if (!project || !projectRef) return null return ( -
+
{isError && } {isSuccess ? ( diff --git a/apps/studio/pages/project/[ref]/storage/policies.tsx b/apps/studio/pages/project/[ref]/storage/policies.tsx index fe93cf7bc35..5c96607f23c 100644 --- a/apps/studio/pages/project/[ref]/storage/policies.tsx +++ b/apps/studio/pages/project/[ref]/storage/policies.tsx @@ -2,18 +2,24 @@ import StorageLayout from 'components/layouts/StorageLayout/StorageLayout' import DefaultLayout from 'components/layouts/DefaultLayout' import { StoragePolicies } from 'components/interfaces/Storage' import type { NextPageWithLayout } from 'types' +import { PageLayout } from 'components/layouts/PageLayout/PageLayout' +import { ScaffoldContainer, ScaffoldSection } from 'components/layouts/Scaffold' const StoragePoliciesPage: NextPageWithLayout = () => { - return ( -
- -
- ) + return } StoragePoliciesPage.getLayout = (page) => ( - {page} + + + {page} + + ) diff --git a/apps/studio/pages/project/[ref]/storage/settings.tsx b/apps/studio/pages/project/[ref]/storage/settings.tsx index c8a6d2ea3e1..b204a9239c5 100644 --- a/apps/studio/pages/project/[ref]/storage/settings.tsx +++ b/apps/studio/pages/project/[ref]/storage/settings.tsx @@ -1,35 +1,27 @@ import DefaultLayout from 'components/layouts/DefaultLayout' import StorageLayout from 'components/layouts/StorageLayout/StorageLayout' -import { - ScaffoldContainer, - ScaffoldDescription, - ScaffoldHeader, - ScaffoldTitle, -} from 'components/layouts/Scaffold' +import { ScaffoldContainer, ScaffoldSection } from 'components/layouts/Scaffold' import { StorageSettings } from 'components/interfaces/Storage' import { S3Connection } from 'components/interfaces/Storage/StorageSettings/S3Connection' import type { NextPageWithLayout } from 'types' +import { PageLayout } from 'components/layouts/PageLayout/PageLayout' -const PageLayout: NextPageWithLayout = () => { +const StorageSettingsPage: NextPageWithLayout = () => { return ( <> - - - Storage Settings - Configure your project's storage settings - - - - - - + + ) } -PageLayout.getLayout = (page) => ( +StorageSettingsPage.getLayout = (page) => ( - {page} + + + {page} + + ) -export default PageLayout +export default StorageSettingsPage diff --git a/apps/studio/pages/sign-in.tsx b/apps/studio/pages/sign-in.tsx index c30d1a314a3..2d727728eba 100644 --- a/apps/studio/pages/sign-in.tsx +++ b/apps/studio/pages/sign-in.tsx @@ -46,7 +46,6 @@ const SignInPage: NextPageWithLayout = () => { or
-
diff --git a/apps/studio/state/ai-assistant-state.tsx b/apps/studio/state/ai-assistant-state.tsx index a9034f084d1..4d2836c070c 100644 --- a/apps/studio/state/ai-assistant-state.tsx +++ b/apps/studio/state/ai-assistant-state.tsx @@ -304,6 +304,19 @@ export const createAiAssistantState = (): AiAssistantState => { } }, + deleteMessagesAfter: (id: string, { includeSelf = true } = {}) => { + const chat = state.activeChat + if (!chat) return + + const messageIndex = chat.messages.findIndex((msg) => msg.id === id) + if (messageIndex === -1) return + + // Delete all messages from the target message (optionally including) to the end + const startIndex = includeSelf ? messageIndex : messageIndex + 1 + chat.messages.splice(startIndex) + chat.updatedAt = new Date() + }, + saveMessage: (message: MessageType | MessageType[]) => { const chat = state.activeChat if (!chat) return @@ -420,6 +433,7 @@ export type AiAssistantState = AiAssistantData & { deleteChat: (id: string) => void renameChat: (id: string, name: string) => void clearMessages: () => void + deleteMessagesAfter: (id: string, options?: { includeSelf?: boolean }) => void saveMessage: (message: MessageType | MessageType[]) => void updateMessage: (args: { id: string; resultId?: string; results: any[] }) => void setSqlSnippets: (snippets: SqlSnippet[]) => void diff --git a/apps/studio/state/storage-explorer.tsx b/apps/studio/state/storage-explorer.tsx index 67e27994112..37356f4555a 100644 --- a/apps/studio/state/storage-explorer.tsx +++ b/apps/studio/state/storage-explorer.tsx @@ -8,6 +8,10 @@ import { proxy, useSnapshot } from 'valtio' import { createClient, SupabaseClient } from '@supabase/supabase-js' import { BlobReader, BlobWriter, ZipWriter } from '@zip.js/zip.js' import { LOCAL_STORAGE_KEYS } from 'common' +import { + inverseValidObjectKeyRegex, + validObjectKeyRegex, +} from 'components/interfaces/Storage/CreateBucketModal.utils' import { STORAGE_BUCKET_SORT, STORAGE_ROW_STATUS, @@ -32,7 +36,7 @@ import { } from 'components/interfaces/Storage/StorageExplorer/StorageExplorer.utils' import { convertFromBytes } from 'components/interfaces/Storage/StorageSettings/StorageSettings.utils' import { InlineLink } from 'components/ui/InlineLink' -import { getKeys, useAPIKeysQuery } from 'data/api-keys/api-keys-query' +import { getTemporaryAPIKey } from 'data/api-keys/temp-api-keys-query' import { configKeys } from 'data/config/keys' import { useProjectSettingsV2Query } from 'data/config/project-settings-v2-query' import { ProjectStorageConfigResponse } from 'data/config/project-storage-config-query' @@ -76,13 +80,11 @@ if (typeof window !== 'undefined') { function createStorageExplorerState({ projectRef, resumableUploadUrl, - serviceKey, supabaseClient, }: { projectRef: string resumableUploadUrl: string - serviceKey: string - supabaseClient?: SupabaseClient + supabaseClient?: () => Promise> }) { const localStorageKey = LOCAL_STORAGE_KEYS.STORAGE_PREFERENCE(projectRef) const { view, sortBy, sortByOrder, sortBucket } = @@ -93,7 +95,6 @@ function createStorageExplorerState({ projectRef, supabaseClient, resumableUploadUrl, - serviceKey, uploadProgresses: [] as UploadProgress[], // abortController, @@ -260,6 +261,17 @@ function createStorageExplorerState({ .join('/') }, + validateFolderName: (name: string) => { + if (!validObjectKeyRegex.test(name)) { + const [match] = name.match(inverseValidObjectKeyRegex) ?? [] + return !!match + ? `Folder name cannot contain the "${match}" character` + : 'Folder name contains an invalid special character' + } + + return null + }, + addNewFolderPlaceholder: (columnIndex: number) => { const isPrepend = true const folderName = 'Untitled folder' @@ -292,22 +304,22 @@ function createStorageExplorerState({ autofix, columnIndex, }) + if (formattedName === null) { onError?.() return } - if (!/^[a-zA-Z0-9_-\s]*$/.test(formattedName)) { - onError?.() - return toast.error( - 'Only alphanumeric characters, hyphens, and underscores are allowed for folder names.' - ) - } - if (formattedName.length === 0) { return state.removeTempRows(columnIndex) } + const folderNameError = state.validateFolderName(formattedName) + if (folderNameError) { + onError?.() + return toast.error(folderNameError) + } + state.updateFolderAfterEdit({ folderName: formattedName, columnIndex }) const emptyPlaceholderFile = `${formattedName}/${EMPTY_FOLDER_PLACEHOLDER_FILE_NAME}` @@ -318,7 +330,7 @@ function createStorageExplorerState({ const formattedPathToEmptyPlaceholderFile = pathToFolder.length > 0 ? `${pathToFolder}/${emptyPlaceholderFile}` : emptyPlaceholderFile - await state.supabaseClient.storage + await (await state.supabaseClient()).storage .from(state.selectedBucket.name) .upload( formattedPathToEmptyPlaceholderFile, @@ -568,7 +580,7 @@ function createStorageExplorerState({ if (data.length === 0) { const prefixToPlaceholder = `${parentFolderPrefix}/${EMPTY_FOLDER_PLACEHOLDER_FILE_NAME}` - await state.supabaseClient?.storage + await (await state.supabaseClient!())?.storage .from(state.selectedBucket.name) .upload(prefixToPlaceholder, new File([], EMPTY_FOLDER_PLACEHOLDER_FILE_NAME)) } @@ -602,7 +614,17 @@ function createStorageExplorerState({ } }, - renameFolder: async (folder: StorageItemWithColumn, newName: string, columnIndex: number) => { + renameFolder: async ({ + folder, + newName, + columnIndex, + onError, + }: { + folder: StorageItemWithColumn + newName: string + columnIndex: number + onError?: () => void + }) => { const originalName = folder.name if (originalName === newName) { return state.updateRowStatus({ @@ -612,24 +634,18 @@ function createStorageExplorerState({ }) } + const folderNameError = state.validateFolderName(newName) + if (folderNameError) { + onError?.() + return toast.error(folderNameError) + } + const toastId = toast( , { closeButton: false, position: 'top-right' } ) try { - /** - * Catch any folder names that contain slash or backslash - * - * this is because slashes are used to denote - * children/parent relationships in bucket - * - * todo: move this to a util file, as createFolder() uses same logic - */ - if (newName.includes('/') || newName.includes('\\')) { - return toast.error(`Folder name cannot contain forward or back slashes.`) - } - state.updateRowStatus({ name: originalName, status: STORAGE_ROW_STATUS.LOADING, @@ -959,20 +975,6 @@ function createStorageExplorerState({ columnIndex: number isDrop?: boolean }) => { - if (!state.serviceKey) { - toast( -

- Uploading files to Storage through the dashboard is currently unavailable with the new - API keys. Please re-enable{' '} - - legacy JWT keys - {' '} - if you'd like to upload files to Storage through the dashboard. -

- ) - return - } - const queryClient = getQueryClient() const storageConfiguration = queryClient .getQueryCache() @@ -1169,9 +1171,7 @@ function createStorageExplorerState({ endpoint: state.resumableUploadUrl, retryDelays: [0, 200, 500, 1500, 3000, 5000], headers: { - authorization: `Bearer ${state.serviceKey}`, 'x-source': 'supabase-dashboard', - ...(state.serviceKey.includes('secret') ? { apikey: state.serviceKey } : {}), }, uploadDataDuringCreation: uploadDataDuringCreation, removeFingerprintOnSuccess: true, @@ -1181,6 +1181,14 @@ function createStorageExplorerState({ ...fileOptions, }, chunkSize, + onBeforeRequest: async (req) => { + try { + const data = await getTemporaryAPIKey({ projectRef: state.projectRef }) + req.setHeader('apikey', data.api_key) + } catch (error) { + throw error + } + }, onShouldRetry(error) { const status = error.originalResponse ? error.originalResponse.getStatus() : 0 const doNotRetryStatuses = [400, 403, 404, 409, 413, 415, 429] @@ -1191,22 +1199,30 @@ function createStorageExplorerState({ numberOfFilesUploadedFail += 1 if (error instanceof tus.DetailedError) { const status = error.originalResponse?.getStatus() - if (status === 415) { - // Unsupported mime type - toast.error( - capitalize( - error?.originalResponse?.getBody() || - `Failed to upload ${file.name}: ${metadata.mimetype} is not allowed` - ), - { - description: `Allowed MIME types: ${state.selectedBucket.allowed_mime_types?.join(', ')}`, - } - ) - } else if (status === 413) { - // Payload too large - toast.error( - `Failed to upload ${file.name}: File size exceeds the bucket upload limit.` - ) + + switch (status) { + case 415: + // Unsupported mime type + toast.error( + capitalize( + error?.originalResponse?.getBody() || + `Failed to upload ${file.name}: ${metadata.mimetype} is not allowed` + ), + { + description: `Allowed MIME types: ${state.selectedBucket.allowed_mime_types?.join(', ')}`, + } + ) + break + case 413: + // Payload too large + toast.error( + `Failed to upload ${file.name}: File size exceeds the bucket upload limit.` + ) + break + case 409: + // Resource already exists + toast.error(`Failed to upload ${file.name}: File name already exists.`) + break } } else { toast.error(`Failed to upload ${file.name}: ${error.message}`) @@ -1293,10 +1309,16 @@ function createStorageExplorerState({ ) { toast.dismiss(toastId) } else if (numberOfFilesUploadedFail === numberOfFilesToUpload) { - toast.error( - `Failed to upload ${numberOfFilesToUpload} file${numberOfFilesToUpload > 1 ? 's' : ''}!`, - { id: toastId, closeButton: true, duration: SONNER_DEFAULT_DURATION } - ) + if (numberOfFilesToUpload === 1) { + // [Joshen] We'd already be showing a toast when the upload files, so this is to prevent a + // duplicate error toast if its only one file that's getting uploaded + toast.dismiss(toastId) + } else { + toast.error( + `Failed to upload ${numberOfFilesToUpload} file${numberOfFilesToUpload > 1 ? 's' : ''}!`, + { id: toastId, closeButton: true, duration: SONNER_DEFAULT_DURATION } + ) + } } else if (numberOfFilesUploadedSuccess === numberOfFilesToUpload) { toast.success( `Successfully uploaded ${numberOfFilesToUpload} file${ @@ -1721,7 +1743,7 @@ function createStorageExplorerState({ ) } />, - { id: toastId, closeButton: false, position: 'top-right' } + { id: toastId, closeButton: false, position: 'top-right', duration: Infinity } ) }, @@ -1764,7 +1786,6 @@ type StorageExplorerState = ReturnType const DEFAULT_STATE_CONFIG = { projectRef: '', resumableUploadUrl: '', - serviceKey: '', supabaseClient: undefined, } @@ -1779,10 +1800,8 @@ export const StorageExplorerStateContextProvider = ({ children }: PropsWithChild const [state, setState] = useState(() => createStorageExplorerState(DEFAULT_STATE_CONFIG)) const stateRef = useLatest(state) - const { data: apiKeys } = useAPIKeysQuery({ projectRef: project?.ref, reveal: true }) const { data: settings } = useProjectSettingsV2Query({ projectRef: project?.ref }) - const { serviceKey } = getKeys(apiKeys) const protocol = settings?.app_config?.protocol ?? 'https' const endpoint = settings?.app_config?.endpoint const resumableUploadUrl = `${IS_PLATFORM ? 'https' : protocol}://${endpoint}/storage/v1/upload/resumable` @@ -1794,43 +1813,39 @@ export const StorageExplorerStateContextProvider = ({ children }: PropsWithChild // Although I'd be keen to re-investigate this to see if we can remove this useEffect(() => { const hasDataReady = !!project?.ref - const serviceApiKey = serviceKey?.api_key ?? 'unknown' if (!isPaused && hasDataReady) { - const clientEndpoint = `${IS_PLATFORM ? 'https' : protocol}://${endpoint}` - const supabaseClient = createClient(clientEndpoint, serviceApiKey, { - auth: { - persistSession: false, - autoRefreshToken: false, - detectSessionInUrl: false, - storage: { - getItem: (key) => { - return null - }, - setItem: (key, value) => {}, - removeItem: (key) => {}, - }, - }, - }) - setState( createStorageExplorerState({ projectRef: project?.ref ?? '', - supabaseClient, + supabaseClient: async () => { + try { + const data = await getTemporaryAPIKey({ projectRef: project.ref }) + const clientEndpoint = `${IS_PLATFORM ? 'https' : protocol}://${endpoint}` + + return createClient(clientEndpoint, data.api_key, { + auth: { + persistSession: false, + autoRefreshToken: false, + detectSessionInUrl: false, + storage: { + getItem: (key) => { + return null + }, + setItem: (key, value) => {}, + removeItem: (key) => {}, + }, + }, + }) + } catch (error) { + throw error + } + }, resumableUploadUrl, - serviceKey: serviceApiKey, }) ) } - }, [ - project?.ref, - stateRef, - serviceKey?.api_key, - isPaused, - resumableUploadUrl, - protocol, - endpoint, - ]) + }, [project?.ref, stateRef, isPaused, resumableUploadUrl, protocol, endpoint]) return ( diff --git a/apps/studio/state/table-editor-table.tsx b/apps/studio/state/table-editor-table.tsx index 197a093cebd..16634adcd7c 100644 --- a/apps/studio/state/table-editor-table.tsx +++ b/apps/studio/state/table-editor-table.tsx @@ -61,8 +61,8 @@ export const createTableEditorTableState = ({ const gridColumns = getInitialGridColumns( getGridColumns(supaTable, { tableId: table.id, - editable, - onAddColumn: editable ? onAddColumn : undefined, + editable: state.editable, + onAddColumn: state.editable ? onAddColumn : undefined, onExpandJSONEditor, onExpandTextEditor, }), @@ -143,6 +143,18 @@ export const createTableEditorTableState = ({ editable, setEditable: (editable: boolean) => { state.editable = editable + + // When changing the editable flag, all grid columns need to be recreated for the editable flag to be propagated. + state.gridColumns = getInitialGridColumns( + getGridColumns(state.table, { + tableId: table.id, + editable, + onAddColumn: editable ? onAddColumn : undefined, + onExpandJSONEditor, + onExpandTextEditor, + }), + { gridColumns: state.gridColumns } + ) }, }) diff --git a/apps/studio/state/table-editor.tsx b/apps/studio/state/table-editor.tsx index be940f4135e..ed325bf861b 100644 --- a/apps/studio/state/table-editor.tsx +++ b/apps/studio/state/table-editor.tsx @@ -26,7 +26,7 @@ export type SidePanel = type: 'foreign-row-selector' foreignKey: ForeignKeyState } - | { type: 'csv-import' } + | { type: 'csv-import'; file?: File } export type ConfirmationDialog = | { type: 'table'; isDeleteWithCascade: boolean } @@ -181,10 +181,10 @@ export const createTableEditorState = () => { sidePanel: { type: 'foreign-row-selector', foreignKey }, } }, - onImportData: () => { + onImportData: (file?: File) => { state.ui = { open: 'side-panel', - sidePanel: { type: 'csv-import' }, + sidePanel: { type: 'csv-import', file }, } }, diff --git a/apps/studio/styles/typography.scss b/apps/studio/styles/typography.scss index 8a050348bec..b284618ace5 100644 --- a/apps/studio/styles/typography.scss +++ b/apps/studio/styles/typography.scss @@ -35,32 +35,32 @@ @layer utilities { /* Heading */ .heading-title { - @apply scroll-m-20 text-2xl tracking-tight text-foreground; + @apply scroll-m-20 text-2xl tracking-tight; } .heading-section { - @apply scroll-m-20 text-xl text-foreground; + @apply scroll-m-20 text-xl; } .heading-subSection { - @apply scroll-m-20 text-base text-foreground; + @apply scroll-m-20 text-base; } .heading-default { - @apply scroll-m-20 text-sm font-medium text-foreground; + @apply scroll-m-20 text-sm font-medium; } .heading-compact { - @apply scroll-m-20 text-xs font-medium text-foreground; + @apply scroll-m-20 text-xs font-medium; } .heading-meta { - @apply text-xs font-mono uppercase tracking-wider font-medium text-foreground; + @apply text-xs font-mono uppercase tracking-wider font-medium; } /* Text */ .text-default { - @apply text-base text-foreground-light; + @apply text-base; } .text-subTitle { diff --git a/apps/studio/types/base.ts b/apps/studio/types/base.ts index 53d471ae055..04847b64c66 100644 --- a/apps/studio/types/base.ts +++ b/apps/studio/types/base.ts @@ -2,9 +2,10 @@ import { PermissionAction } from '@supabase/shared-types/out/constants' import { OrganizationBase } from 'data/organizations/organizations-query' import { PlanId } from 'data/subscriptions/types' import jsonLogic from 'json-logic-js' +import { ManagedBy } from 'lib/constants/infrastructure' export interface Organization extends OrganizationBase { - managed_by: 'supabase' | 'vercel-marketplace' | 'aws-marketplace' + managed_by: ManagedBy partner_id?: string plan: { id: PlanId; name: string } } diff --git a/apps/studio/vitest.config.ts b/apps/studio/vitest.config.ts index 964b7ff991a..4091d3e7cad 100644 --- a/apps/studio/vitest.config.ts +++ b/apps/studio/vitest.config.ts @@ -1,6 +1,6 @@ import { resolve } from 'node:path' import { fileURLToPath } from 'node:url' -import { defineConfig } from 'vitest/config' +import { configDefaults, defineConfig } from 'vitest/config' import react from '@vitejs/plugin-react' import tsconfigPaths from 'vite-tsconfig-paths' @@ -26,9 +26,11 @@ export default defineConfig({ environment: 'jsdom', // TODO(kamil): This should be set per test via header in .tsx files only setupFiles: [ resolve(dirname, './tests/vitestSetup.ts'), - resolve(dirname, './tests/setup/polyfills.js'), + resolve(dirname, './tests/setup/polyfills.ts'), resolve(dirname, './tests/setup/radix.js'), ], + // Don't look for tests in the nextjs output directory + exclude: [...configDefaults.exclude, `.next/*`], reporters: [['default']], coverage: { reporter: ['lcov'], diff --git a/apps/www/_alternatives/supabase-vs-firebase.mdx b/apps/www/_alternatives/supabase-vs-firebase.mdx index 3bb86c83e1f..17470aa777a 100644 --- a/apps/www/_alternatives/supabase-vs-firebase.mdx +++ b/apps/www/_alternatives/supabase-vs-firebase.mdx @@ -1,69 +1,121 @@ --- title: Supabase vs Firebase description: Supabase is the Postgres development platform with a SQL based Database, Auth, and Cloud Functions -author: ant_wilson +author: prashant tags: - comparison -date: '2022-05-26' +date: '2025-08-20' toc_depth: 3 --- -## What is Firebase? - -Now owned by Google, Firebase is a collection of tools aimed at mobile and web developers. At its core is the Firestore database. - -Firestore allows you to store “documents”. These are collections of key:value pairs where the value can be another sub-document. Document based storage is perfect for unstructured data, since two documents in a collection do not necessarily need to have the same structure. - -Firebase also offers other things that web developers find useful like an auth service for user management, and wrappers for other Google services such as Cloud Functions, and File Storage. +Firebase and Supabase both promise to give teams a full‑featured backend without managing servers. Firebase, owned by Google, combines a NoSQL database with authentication, file storage and serverless functions. Supabase is an open‑source alternative [built on Postgres](/docs/guides/database/overview) that offers similar services—[auth](/docs/guides/auth), [storage](/docs/guides/storage), [real‑time](/docs/guides/realtime), and [functions](/docs/guides/functions), but with the predictability of SQL and the freedom to self‑host. Choosing between them depends on how your application models data, how you plan to scale, and your tolerance for vendor lock‑in. ## What is Supabase? -Supabase is the Postgres development platform. Instead of being built around a document-based datastore, Supabase offers a relational database management system called PostgreSQL. This comes with a few advantages: +Supabase is an open‑source backend platform that runs on **Postgres**. Each project comes with a dedicated Postgres database, an auto‑generated REST and GraphQL API, real‑time subscriptions, and storage, all tied together with **Row‑Level Security (RLS)**. RLS policies are written in SQL and allow you to define granular access rules using the same language you use for your data. Because Supabase’s core services are built on open technologies like Postgres and GoTrue, you can run Supabase locally or host it yourself using Docker or community tools such as Kubernetes or Terraform. -- It’s open source, so there is zero lock in. -- You can query it with SQL, a proven and powerful query language. -- It has a long track record of being used at scale. -- It’s the database of choice for transactional workloads (think apps and websites, or other things that require near-instant responses to queries). -- It comes with decades of [useful postgres extensions and plug-ins](https://supabase.com/docs/guides/database/extensions). +## What is Firebase? -At Supabase we’ve always been huge fans of Firebase - so we started adding a few things on top of PostgreSQL in an attempt to reach feature parity, including: +Firebase is a managed platform owned by Google. It includes Cloud Firestore (a document‑oriented NoSQL database), authentication, Cloud Storage for files and Cloud Functions for backend logic. Firestore stores data as JSON‑like documents grouped in collections. The SDKs automatically cache documents on the device so apps can read, write and listen to data while offline; changes are synchronized when the device reconnects[.](https://firebase.google.com/docs/firestore/manage-data/enable-offline#:~:text=Cloud%20Firestore%20supports%20offline%20data,to%20the%20Cloud%20Firestore%20backend) Firestore supports *collection group queries*, which let you query across collections with the same name. To handle relational data you often denormalize documents or perform multiple queries in code. -- Auto-generated API - [query your data straight from the client](https://supabase.com/docs/guides/api#rest-api-overview). -- Realtime - [changes in your data will be streamed directly to your application](https://supabase.com/docs/reference/dart/subscribe). -- Auth - [a simple to integrate auth system and SQL based rules engine](https://supabase.com/auth). -- Functions - [javascript and typescript functions that deploy out globally](https://supabase.com/edge-functions). -- Storage - [hosting images, videos, and pdfs easily](https://supabase.com/storage). +## Core architecture and database -## How are they similar? +| Feature | Firebase | Supabase | +| ------------------- | ----------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------ | +| **Database type** | Document database (Cloud Firestore) | Relational database (Postgres) | +| **Data model** | Schemaless JSON; subcollections; no native joins | Structured tables with foreign keys and indexes | +| **Transactions** | Supports transactions and batched writes | Full ACID transactions built into Postgres; you can execute complex joins, subqueries and data transformations in SQL | +| **Offline support** | Firestore caches actively used data on the client so you can read/write while offline; changes sync when connectivity returns | Supabase clients use Postgres directly; offline access requires your own caching strategy and works with Zero, Electric, | +| **API access** | Client SDKs for web, Android, iOS and server environments; REST/gRPC endpoints | Auto‑generated REST API via PostgREST and GraphQL API via pg_graphql | +| **Self‑hosting** | Not supported; services run on Google Cloud | Fully self‑hostable via Docker or on your own cloud | -Both Firebase and Supabase are based on the idea of bringing a superior developer experience to databases. With both platforms you can spin up a new project from directly inside the browser without the need to download any extra tools or software to your machine. Both platforms come with a useful dashboard UI for debugging your data in realtime, which is especially useful for fast iterations when in development. +Firestore’s schemaless design makes it easy to prototype. As data relationships grow more complex, you must handle joins in the client or denormalize your data. Supabase’s Postgres foundation gives you SQL’s expressive power, foreign keys, and indexes from day one. -Both Firebase and Supabase have invested heavily in client side libraries so you can communicate with your database directly from the client. Firebase has their [Firebase Javascript SDK](https://github.com/firebase/firebase-js-sdk) and Supabase has [supabase-js an isomorphic client](https://github.com/supabase/supabase-js/) that can be used both on the client also on the server in a node-js environment. +## Authentication and user management -## How are they different? +| Feature | Firebase | Supabase | +| ------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------ | +| **Login methods** | Email/password, phone, anonymous and OAuth providers; additional SAML and OpenID Connect support requires upgrading to *Firebase Authentication with Identity Platform* | Email/password, OAuth providers (Google, GitHub, Apple, etc.), SMS and custom providers | +| **Enterprise auth** | The Identity Platform upgrade enables multi‑factor auth, SAML and OIDC | SSO, SAML and OIDC are available on Pro and Enterprise plans without changing SDKs | +| **Access control** | Security Rules use a domain‑specific language and differ for Firestore, Storage and Functions | Row‑Level Security (RLS) policies written in SQL control access across the database | +| **Offline auth** | The client SDK persists auth tokens locally and handles session renewal | Session management integrates with Postgres; tokens are validated on the server | +| **Customization** | Custom auth requires writing Cloud Functions or backend services | You can write custom policies and logic in SQL; column‑level security enables field‑level restrictions | -Firebase and Supabase differ in several ways. The main one being that Firebase is a document store, whereas Supabase is based on PostgreSQL - a relational, SQL-based database management system. +Firebase Authentication is easy to set up for email/password and social logins. Enterprise features such as SAML/OIDC and blocking functions require upgrading to the Identity Platform and come with new usage limits[.](https://firebase.google.com/docs/auth#:~:text=iOS%20%20%20135%20Web,192%20Unity) Supabase Auth integrates with Postgres, so you can enforce complex policies using SQL and upgrade to SSO or enterprise auth features without changing products. -There are some other important differences. +## Serverless functions and backend logic -### Open Source +| Feature | Firebase | Supabase | +| -------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------- | +| **Runtime** | Cloud Functions run on Google’s infrastructure. You write code in JavaScript or TypeScript and deploy through the Firebase CLI; Python support is available in the second‑generation runtime | Edge Functions run on Deno and are globally distributed at the edge for low‑latency execution | +| **Languages** | JavaScript, TypeScript and Python (2nd gen) | TypeScript (via Deno); you can also call third‑party APIs or write SQL from functions | +| **Triggers** | HTTP requests, Firestore, auth events, Storage, pub/sub and scheduled jobs | HTTP requests, scheduled jobs and other triggers; functions can call Postgres directly through the `supabase-js` client | +| **Cold start** | Functions run in a managed environment; cold starts can vary depending on region and load | Edge Functions start quickly because Deno’s runtime is lightweight and runs close to users | -Supabase is open source. Along with the hosted cloud platform, you can also take the Supabase stack and host it inside your own cloud or run it locally on your machine. There is no vendor lock in. +Firebase Cloud Functions integrate tightly with other Firebase products and support a wide range of triggers. Supabase Edge Functions are TypeScript functions that run on Deno at edge locations. They talk directly to your Postgres database, which simplifies tasks like handling webhooks or building custom APIs. -### Pricing +## Storage and file management -[Firebase charges for reads, writes and deletes](https://firebase.google.com/pricing), which can lead to some unpredictability, especially in the early stages of a project when your application is in heavy development. Supabase [charges based on the amount of data stored](https://supabase.com/pricing), with breathing room for unlimited API requests and an unlimited number of Auth users. +| Feature | Firebase | Supabase | +| ----------------------- | --------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------- | +| **Underlying platform** | Cloud Storage for Firebase (built on Google Cloud Storage) | S3‑compatible object store integrated with Postgres | +| **Resumable uploads** | The web SDK provides `uploadBytesResumable()`and methods to pause, resume or cancel an upload | Built‑in support for resumable uploads; each upload is recorded in a Postgres table | +| **Access control** | Security Rules use a separate rules language for Storage | Storage metadata is stored in Postgres and protected by the same RLS policies as the rest of your data | +| **Image tools** | No built‑in transformations (you can add Firebase Extensions for resizing) | Built‑in image transformations and CDN delivery | +| **Free tier** | Spark plan includes 5 GB of Cloud Storage in select regions; additional usage is billed by region and operation | Free plan allows files up to 50 MB and 1 GB total storage; Pro/Team plans allow up to 500 GB per file and include CDN | -### Performance +Firebase Storage is reliable and integrates with the auth SDK. Pricing depends on region and includes separate charges for storage, bandwidth and operations. Supabase Storage integrates with Postgres: metadata is stored in a table, so you can apply the same RLS policies you use elsewhere[.](/docs/guides/storage/security/access-control#:~:text=Supabase%20Storage%20is%20designed%20to,RLS) Free plans include 50 MB per file, and Pro plans raise that limit to hundreds of gigabytes. -We created a benchmarking repo where you can compare the performance of both services in different scenarios. Our most recent results show that [Supabase outperforms Firebase by up to 4x](https://github.com/supabase/benchmarks/issues/8) on number of reads per second, and 3.1x on writes per second. +## Open source vs. proprietary -## How do I migrate from Firebase to Supabase? +| Feature | Firebase | Supabase | +| ------------------ | --------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------- | +| **Core platform** | Proprietary managed services hosted by Google; client SDKs are open source. No self-hosting option. | Fully open source; core components such as Postgres, GoTrue and PostgREST are licensed under permissive licenses, and you can run Supabase yourself | +| **Self‑hosting** | Not available | Supported via CLI and Docker; community tools exist for Kubernetes, Terraform and other platforms | +| **Governance** | Maintained by Google | Community‑driven with a public roadmap and contribution model | +| **Vendor lock‑in** | Tight coupling to Google Cloud products | Portable architecture; you can move between Supabase Cloud and self‑hosted deployments without rewriting your app | -Since Firebase is document based, migrating into a relational database requires you to map your data structure across into a SQL schema. Luckily we’ve built a [handy conversion tool to do it for you](https://supabase.com/docs/guides/migrations/firestore-data). +Firebase provides a polished, fully managed experience. Supabase embraces open source: you can inspect the code, contribute to improvements and, if needed, host your own Supabase instance. -We also have guides and tools for [migrating Firebase Auth to Supabase Auth](https://supabase.com/docs/guides/migrations/firebase-auth) for [migrating Firebase Storage files to Supabase Storage](https://supabase.com/docs/guides/migrations/firebase-storage). +## Pricing and cost comparison -These are by far the most complete Firebase to Postgres migration tools available anywhere on the web. +| Feature | Firebase | Supabase | +| ----------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------ | +| **Free tier** | Spark plan includes a limited free quota—for example, Firestore allows 50,000 document reads and 20,000 writes per day, Storage includes 5 GB in select regions and Cloud Functions are limited | Supabase’s free plan includes unlimited API requests, 50,000 monthly active users, 500 MB database storage, 1 GB file storage and 5 GB bandwidth | +| **Billing model** | Pay‑as‑you‑go; you pay per document read, write, delete and per function invocation[.](https://firebase.google.com/docs/firestore/pricing#:~:text=Document%20reads%2050%2C000%20per%20day,transfer%2010%20GiB%20per%20month) Pricing varies by region and can be hard to predict | Transparent tiered pricing: pay for database storage, file storage, and compute. There are no charges for API requests, and you can track usage from the dashboard | +| **Self‑hosting** | Not supported | Available—self‑hosting lets you control costs and comply with regulatory requirements | -You can [try Supabase for free](https://supabase.com/dashboard). If you require Enterprise level support with your project or migration, please get in touch using our [Enterprise contact form](https://forms.supabase.com/enterprise). +Firebase’s usage‑based pricing can surprise teams as their apps grow because every document read, write or listener contributes to cost[.](https://firebase.google.com/docs/firestore/pricing#:~:text=Document%20reads%2050%2C000%20per%20day,transfer%2010%20GiB%20per%20month) Supabase offers predictable tiers and does not bill per request, which can simplify budgeting[.](/pricing#:~:text=Get%20started%20with%3A) + +## Ecosystem, extensibility and community + +| Feature | Firebase | Supabase | +| ------------------------ | --------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------- | +| **Platform integration** | Deep integration with Google Cloud services such as BigQuery, Cloud Functions and Firebase ML | Built on Postgres; you can leverage Postgres extensions, listen to database events and integrate with third‑party services | +| **Extensions** | Firebase Extensions provide pre‑built functionality (e.g., image resizing, Stripe sync) | Postgres extensions (50+ preconfigured) add features like full‑text search, vector similarity and custom data types | +| **SDKs** | Official SDKs for web, Android, iOS, Unity, C++ and more | Official JavaScript client and community clients for Go, Rust, Dart and other languages | +| **Community** | Support via GitHub issues, Stack Overflow and Firebase Summit | Active open‑source community with GitHub discussions, the SupaSquad advocacy program and third‑party integrations | +| **Self‑hosting tools** | None | Community‑maintained packages for Terraform, Kubernetes and BYO cloud | + +Firebase benefits from Google’s ecosystem and has mature SDKs for many platforms. Supabase leverages the Postgres ecosystem and open‑source contributions; you can extend it with Postgres extensions or third‑party packages. + +## Scalability and performance + +Firestore scales automatically and handles real‑time synchronization for simple use cases. Queries are shallow and cannot traverse relationships; collection group queries allow searching across collections with the same name[.](https://firebase.blog/posts/2019/06/understanding-collection-group-queries/#:~:text=In%20the%20past%2C%20you%20could,are%20in%20a%20single%20subcollection) Firestore caches data on the client so offline access is good. Deep querying often requires multiple reads or denormalized data, which can increase costs[.](https://firebase.google.com/docs/firestore/pricing#:~:text=Document%20reads%2050%2C000%20per%20day,transfer%2010%20GiB%20per%20month) + +Supabase inherits Postgres’s mature scaling features. You can perform joins, subqueries and full‑text search in a single SQL statement. Transactions guarantee consistency, and indexes keep queries fast. Real‑time updates are delivered via Postgres logical replication, and connection pooling and read replicas can be added as your load grows. Because Supabase is built on a relational database, data remains strongly consistent, and performance tuning is well understood in the Postgres ecosystem. + +## Migrating from Firebase to Supabase + +Many teams start with Firebase for fast prototyping and switch to Supabase when their data model becomes relational or costs become unpredictable. A typical migration involves two phases: + +1. **Run both services side by side.** Export your Firestore collections and import them into Supabase—initially as JSON or JSONB columns—while your app continues to read from Firebase. Then incrementally swap Firebase SDK calls for Supabase queries. +2. **Normalize your data.** Refactor JSON into proper Postgres tables, add foreign keys and indexes and write RLS policies. This is when you begin to enjoy SQL’s power and simplified code. + +Supabase provides [open‑source migration tools](/docs/guides/platform/migrating-to-supabase). There is a utility for [migrating Firebase Authentication to Supabase Auth](/docs/guides/platform/migrating-to-supabase/firebase-auth) by exporting users and re‑creating them in Postgres, another for [copying Firestore collections to Postgres tables](/docs/guides/platform/migrating-to-supabase/firestore-data), and a tool for [migrating files from Cloud Storage to Supabase Storage](/docs/guides/platform/migrating-to-supabase/firebase-storage). These tools are maintained on GitHub and have been used by startups to migrate tens of thousands of users with minimal downtime. + +## Conclusion + +Firebase offers an excellent developer experience for prototypes and simple applications. It excels at real‑time synchronization and has a generous free tier. Supabase provides a similar developer experience with a relational core. By building on Postgres and open‑source components, Supabase gives you SQL queries, ACID transactions, RLS and the option to self‑host. For teams that anticipate complex data relationships, need fine‑grained access control, or want predictable costs and open‑source flexibility, Supabase is a compelling alternative. + +Need help migrating from Firebase to Supabase? [Contact](https://forms.supabase.com/firebase-migration) our Firebase Migration Team. diff --git a/apps/www/_blog/2025-07-14-jwt-signing-keys.mdx b/apps/www/_blog/2025-07-14-jwt-signing-keys.mdx index 168a61242e0..7fd4efac42e 100644 --- a/apps/www/_blog/2025-07-14-jwt-signing-keys.mdx +++ b/apps/www/_blog/2025-07-14-jwt-signing-keys.mdx @@ -22,7 +22,7 @@ Today we're announcing some long awaited changes in Supabase: