diff --git a/apps/docs/DEVELOPERS.md b/apps/docs/DEVELOPERS.md new file mode 100644 index 00000000000..50ea5d9deec --- /dev/null +++ b/apps/docs/DEVELOPERS.md @@ -0,0 +1,98 @@ +# Developing Supabase Docs + +## Getting started + +Thanks for your interest in [Supabase docs](https://supabase.com/docs) and for wanting to contribute! Before you begin, read the +[code of conduct](https://github.com/supabase/.github/blob/main/CODE_OF_CONDUCT.md) and check out the +[existing issues](https://github.com/supabase/supabase/issues). +This document describes how to set up your development environment to contribute to [Supabase docs](https://supabase.com/docs). + +For a complete run-down on how all of our tools work together, see the main DEVELOPERS.md. That readme describes how to get set up locally in lots of detail, including minimum requirements, our Turborepo setup, installing packages, sharing components across projects, and more. This readme deals specifically with the docs site. + +## Local setup + +[supabase.com/docs](https://supabase.com/docs) is a Next.JS site. You can get setup by following the same steps for all of our other Next.JS projects: + +1. Follow the steps outlined in the Local Development section of the main [DEVELOPERS.md](https://github.com/supabase/supabase/blob/master/DEVELOPERS.md) +2. Start the local docs site by navigating to `/apps/docs` and running `npm run dev` +3. Visit http://localhost:3001/docs in your browser - don't forget to append the `/docs` to the end +4. Your local site should look exactly like [https://supabase.com/docs](https://supabase.com/docs) + +## Types of documentation + +[https://supabase.com/docs](https://supabase.com/docs) has several different kinds of documentation, all coming from different sources. + +### Guides + +The primary, instructional type of content. Basically anything that lives on the `https://supabase.com/docs/guides` route. This includes Guides for Auth, Database, Storage, Realtime, Edge Functions, as well as general resources, self-hosting instructions, and integrations. These are all [`.mdx`](https://mdxjs.com/) files — a combination of Markdown and Javascript. + +#### Things to know + +Here's a simple [example](https://supabase.com/docs/guides/functions) `.mdx` Guide, and here is [the source on Github](https://raw.githubusercontent.com/supabase/supabase/master/apps/docs/pages/guides/functions.mdx). + +Some things to note: + +1. The files need to import a Layout at the top +2. The files need to export a `Page` at the bottom with the `` component +3. The files frontmatter is stored in `const meta = {}`. You should always include `title` and `description`. +4. You can write Markdown as you normally would, but you can also write regular Javascript and JSX. Note the `examples` array that we iterate over. +5. Any Javascript variables you use in these files need to be exported in order to be used (i.e., `export const examples = []`). + +##### Using components + +You can use any standard React components in these `.mdx` files without having to explicitly import them in each file. All components get imported in a [common components](https://github.com/supabase/supabase/blob/master/apps/docs/components/index.tsx) file and can be used in any `.mdx` file. Components can also be "intercepted" and modified via this file. Note how we're intercepting the `h2`, `h3` and `code` tags and modifying them before converting the `mdx` to `html`. + +### Reference docs for client libraries + +We maintain client libraries for [Javascript](https://supabase.com/docs/reference/javascript) and [Flutter/Dart](https://supabase.com/docs/reference/dart) (with more to come). These reference docs document every object and method available for developers to use. The are assembled from different sources and work much differently than the `.mdx` Guides we just looked at. + +The client libraries are essentially wrappers around the clients for the various tools we use — GoTrue, PostgREST, Storage, Functions, and Realtime. The easiest way to describe how the things fit together is to look at an example and trace where the various pieces of information are coming from. + +#### Example + +Let's look at the `updateUser()` function in the `supabase-js` library. + +#### Common file + +Several pieces of information for this function come from a [common file](https://github.com/supabase/supabase/blob/3d774b3b7bcdcb410e25726d832467584ebea686/spec/common-client-libs-sections.json#L548) where we store information shared by all libraries. + +1. id — used to identify this function +2. title - the human-readable title +3. slug — the url slug +4. product - the Supabase tool or product that "owns" this function. Since `updateUser()` is an auth function, its product is `auth` +5. type — `updateUser()` is a function and marked as such, but we can also have sections of markdown interspersed with these function definitions. + +When a new function is added, this info would need to be manually added to the common file. + +#### Function Parameters + +The `updateUser()` function takes one parameter: `attributes`. The details for this parameter live in the GoTrue client library, referenced via a `$ref` property in the `supabase-js` [spec file](https://github.com/supabase/supabase/blob/cb04d85262db6a371539dda7df9b00ba5a901e87/spec/supabase_js_v2.yml#L357). Here, the `$ref` property is pointing to the [actual function definition](https://github.com/supabase/gotrue-js/blob/2d60e79073b96ae8c97a6ce18e2601ed1e2a2712/src/GoTrueClient.ts#L590) in the `gotrue-js` library. The accepted values for the `attributes` parameter come from the [type definition](https://github.com/supabase/gotrue-js/blob/16d3deb822097e8640a3a15b94a5690b3beaf11b/src/lib/types.ts#L233). + +These individual library spec files are fetched via this [Makefile](https://github.com/supabase/supabase/blob/master/spec/Makefile), and get [transformed](https://github.com/supabase/supabase/blob/master/spec/enrichments/tsdoc_v2/supabase_dereferenced.json) to combine the information we need (params, types, etc). Unless you're a library maintainer, you shouldn't need to worry about this part of the process. + +If you are a library maintainer, the last important note about these library files is that the [Makefile](https://github.com/supabase/supabase/blob/master/spec/Makefile) pulls from the `gh-pages` branch of the client library repo. Here's an example of the [`realtime-js` spec file](https://github.com/supabase/realtime-js/blob/gh-pages/v2/spec.json). Updating something like function params or returns, the process is: + +1. Get your changes merged to `master` in your library +2. This will kick off an action that automatically updates the spec file in the library's `gh-pages` branch +3. Run `make` in `/spec` of the `supabase/supabase` repo. This will regenerate all of the `tsdoc` files that the docs site uses +4. You should now see the changes you've made in the docs site locally + +#### Function Examples + +The `updateUser()` function has three examples listed with it. The examples are stored along with the `$ref` property in the [supabase_js_v2 spec file](https://github.com/supabase/supabase/blob/master/spec/supabase_js_v2.yml). + +#### Rendering in Next.JS + +These reference docs are rendered by Next.JS via a dynamic route using a [`[...slug.tsx]`](https://github.com/supabase/supabase/blob/master/apps/docs/pages/reference/javascript/%5B...slug%5D.tsx). Here, we use the library [spec file](https://github.com/supabase/supabase/blob/bd0514553c627db8f1e8d0b3ae440ccb6759d228/apps/docs/pages/reference/javascript/%5B...slug%5D.tsx#L4) and the [common file](https://github.com/supabase/supabase/blob/bd0514553c627db8f1e8d0b3ae440ccb6759d228/apps/docs/pages/reference/javascript/%5B...slug%5D.tsx#L1) to output the info you see on the page. + +### Other reference docs + +The reference docs for the [Supabase Management API](https://supabase.com/docs/reference/api) and the [Supabase CLI](https://supabase.com/docs/reference/cli) are a little more straightforward than the client libraries. Both files also have a [common file](https://github.com/supabase/supabase/blob/master/spec/common-cli-sections.json) which handles things like `title`, `id` and `slug`. Both also have a spec file detailing things like parameters, descriptions, and responses ([Management API](https://github.com/supabase/supabase/blob/master/spec/api_v0_openapi.json) / [CLI](https://github.com/supabase/supabase/blob/master/spec/cli_v1_commands.yaml)) + +On the Next.JS side of things, these work almost exactly the same as the client libaries with a dynamic [`[...slug.tsx]`](https://github.com/supabase/supabase/blob/master/apps/docs/pages/reference/cli/%5B...slug%5D.tsx). + +### Misc + +#### Search + +Search is handled through Algolia. When the site is built, a [search script](https://github.com/supabase/supabase/blob/master/apps/docs/scripts/build-search.ts) runs through all of the types of content, generating search objects that are sent to Algolia to index. diff --git a/apps/docs/components/Navigation/Navigation.constants.ts b/apps/docs/components/Navigation/Navigation.constants.ts index cd670c149ac..b0d769e6ea3 100644 --- a/apps/docs/components/Navigation/Navigation.constants.ts +++ b/apps/docs/components/Navigation/Navigation.constants.ts @@ -305,6 +305,7 @@ export const menuItems: NavMenu = { { name: 'Authsignal', url: '/guides/integrations/authsignal', items: [] }, { name: 'Clerk', url: '/guides/integrations/clerk', items: [] }, { name: 'keyri', url: '/guides/integrations/keyri', items: [] }, + { name: 'Picket', url: '/guides/integrations/picket', items: [] }, { name: 'Stytch', url: '/guides/integrations/stytch', items: [] }, { name: 'SuperTokens', url: '/guides/integrations/supertokens', items: [] }, ], diff --git a/apps/docs/components/Navigation/NavigationMenu/NavigationMenuRefListItems.tsx b/apps/docs/components/Navigation/NavigationMenu/NavigationMenuRefListItems.tsx index 1611cd51992..123a34e27ad 100644 --- a/apps/docs/components/Navigation/NavigationMenu/NavigationMenuRefListItems.tsx +++ b/apps/docs/components/Navigation/NavigationMenu/NavigationMenuRefListItems.tsx @@ -257,20 +257,18 @@ const Content: React.FC = ({ id, lib, commonSections, sp {fn.items && - fn.items - //.filter((item) => item.libs.includes(lib)) - .map((item) => ( - - ))} + fn.items.map((item) => ( + + ))} ) : ( diff --git a/apps/docs/components/Options.tsx b/apps/docs/components/Options.tsx index c2783edeaa4..e349cdd97bb 100644 --- a/apps/docs/components/Options.tsx +++ b/apps/docs/components/Options.tsx @@ -70,7 +70,9 @@ const Option: FC = (props) => { {props.type ?? 'no type'} -

{props.description ?? 'nodescription'}

+ + {props.description &&

{props.description}

} + {props.children} ) diff --git a/apps/docs/components/Params.tsx b/apps/docs/components/Params.tsx index ede7f8bcac7..10994966198 100644 --- a/apps/docs/components/Params.tsx +++ b/apps/docs/components/Params.tsx @@ -20,7 +20,9 @@ const Param: FC = (paramItem) => { {paramItem.type ?? 'no type'} -

{paramItem.description ?? 'nodescription'}

+ {paramItem.description && ( +

{paramItem.description}

+ )} {paramItem.children} ) diff --git a/apps/docs/components/index.tsx b/apps/docs/components/index.tsx index 9ee8c4060d5..587a5cfee32 100644 --- a/apps/docs/components/index.tsx +++ b/apps/docs/components/index.tsx @@ -1,4 +1,5 @@ -import { Button, Tabs, Alert } from 'ui' +import { Button, Tabs, Alert, GlassPanel } from 'ui' +import Link from 'next/link' // Common components import Admonition from './Admonition' @@ -38,6 +39,8 @@ const components = { Button, ButtonCard, CodeBlock, + GlassPanel, + Link, Frameworks, AuthProviders, FunctionsExamples, diff --git a/apps/docs/components/reference/CLICommandSection.tsx b/apps/docs/components/reference/CLICommandSection.tsx index 9f685157482..f7e86190b4e 100644 --- a/apps/docs/components/reference/CLICommandSection.tsx +++ b/apps/docs/components/reference/CLICommandSection.tsx @@ -16,7 +16,7 @@ interface ICommonFunc { } interface IRefFunctionSection { - funcData: any + funcData: ICommonFunc commonFuncData: ICommonFunc spec: any } diff --git a/apps/docs/components/reference/RefFunctionSection.tsx b/apps/docs/components/reference/RefFunctionSection.tsx index 5cdacd0d458..14f74a45c44 100644 --- a/apps/docs/components/reference/RefFunctionSection.tsx +++ b/apps/docs/components/reference/RefFunctionSection.tsx @@ -10,22 +10,7 @@ import { extractTsDocNode, generateParameters } from '~/lib/refGenerator/helpers import RefDetailCollapse from '~/components/reference/RefDetailCollapse' import { Fragment } from 'react' - -interface ICommonFunc { - id: string - title: string - slug: string - product: string - libs: string - items: ICommonFunc[] -} - -interface IRefFunctionSection { - funcData: any - commonFuncData: ICommonFunc - spec: any - typeSpec?: any -} +import { IRefFunctionSection } from './Reference.types' const RefFunctionSection: React.FC = (props) => { const item = props.spec.functions.find((x: any) => x.id === props.funcData.id) @@ -204,7 +189,6 @@ const RefFunctionSection: React.FC = (props) => { )} - { +const RefSectionHandler = (props: RefSectionHandlerProps) => { const router = useRouter() const slug = router.query.slug[0] - const isNewDocs = process.env.NEXT_PUBLIC_NEW_DOCS === 'true' - // When user lands on a url like http://supabase.com/docs/reference/javascript/sign-up // find the #sign-up element and scroll to that useEffect(() => { - if (isNewDocs && document && slug !== 'start') { + if (document && slug !== 'start') { document.querySelector(`#${slug}`) && document.querySelector(`#${slug}`).scrollIntoView() } }) diff --git a/apps/docs/components/reference/Reference.types.ts b/apps/docs/components/reference/Reference.types.ts new file mode 100644 index 00000000000..04a9c04b7da --- /dev/null +++ b/apps/docs/components/reference/Reference.types.ts @@ -0,0 +1,109 @@ +import { enrichedOperation } from '~/lib/refGenerator/helpers' + +export interface ISpec { + openref: any + info: { + id: string + title: string + description: string + definition: string + libraries: any + slugPrefix: string + specUrl: string + } + functions: IFunctionDefinition[] +} + +export interface IAPISpec { + info: { + title: string + description?: string + version: string + contact?: {} + } + operations: enrichedOperation[] + sections: any +} + +export interface IFunctionDefinition { + title: string + id: string + $ref: string + description: string + examples?: [] +} + +export interface ICommonFunc { + id: string + title: string + slug: string + product: string + type: string + parent?: string + items: ICommonFunc[] +} + +export interface IRefFunctionSection { + funcData: any + commonFuncData: ICommonFunc + spec: any + typeSpec?: TypeSpec +} + +export interface IRefStaticDoc { + id: string + title: string + meta: { + id: string + title: string + hideTitle: boolean + } + content: { + compiledSource: string + frontmatter: {} + scope: {} + } +} + +export type TypeSpec = { + name: string + children: TypeSpecChild[] +} + +export type TypeSpecChild = { + id: number + name: string + kind: number + kindString?: string + flags?: {} + originalName?: string + children?: TypeSpecChild[] + defaultValue?: string + default?: any + groups?: any + sources?: any + target?: any + comment?: TypeSpecChildComment + typeParameter?: any + implementedTypes?: any + extendedTypes?: any + dereferenced?: TypeSpecChild + extendedBy?: any + indexSignature?: any + extendsType?: any + objectType?: any + trueType?: any + falseType?: any + type?: any + signatures?: any + overwrites?: any + inheritedFrom?: any + implementationOf?: any +} + +export interface TypeSpecChildComment { + shortText?: string + text?: string | null + returns?: string | null + tags?: any +} diff --git a/apps/docs/lib/mdx/handleRefStaticProps.tsx b/apps/docs/lib/mdx/handleRefStaticProps.tsx index c9c47b4496d..c97245387f9 100644 --- a/apps/docs/lib/mdx/handleRefStaticProps.tsx +++ b/apps/docs/lib/mdx/handleRefStaticProps.tsx @@ -1,33 +1,12 @@ -import generateOldRefMarkdown from '~/lib/mdx/generateOldRefMarkdown' import generateRefMarkdown from '~/lib/mdx/generateRefMarkdown' -async function handleRefStaticProps(sections, params, librarypath, urlPath) { +async function handleRefStaticProps(sections, librarypath) { let markdownContent = await generateRefMarkdown(sections, librarypath) - /* - * old content generation - * this is for grabbing to old markdown files - */ - - let slug - if (params.slug.length > 1) { - slug = `docs/reference${urlPath}/${params.slug.join('/')}` - } else { - slug = `docs/reference${urlPath}/${params.slug[0]}` - } - - /* - * handle old ref pages - */ - if (process.env.NEXT_PUBLIC_NEW_DOCS === 'false') { - const oldMarkdown = await generateOldRefMarkdown(slug) - return oldMarkdown - } else { - return { - props: { - docs: markdownContent, - }, - } + return { + props: { + docs: markdownContent, + }, } } diff --git a/apps/docs/lib/refGenerator/helpers.ts b/apps/docs/lib/refGenerator/helpers.ts index 9c14a80167f..83864fa5540 100644 --- a/apps/docs/lib/refGenerator/helpers.ts +++ b/apps/docs/lib/refGenerator/helpers.ts @@ -218,6 +218,12 @@ export type enrichedOperation = OpenAPIV3.OperationObject & { operationId: string operation: string responseList: [] + description?: string + parameters?: [] + responses?: {} + security?: [] + summary?: string + tags?: [] } export function gen_v3(spec: OpenAPIV3.Document, dest: string, { apiUrl }: { apiUrl: string }) { diff --git a/apps/docs/pages/guides/auth/auth-helpers/nextjs-server-components.mdx b/apps/docs/pages/guides/auth/auth-helpers/nextjs-server-components.mdx index 35861d98c86..a49f6c3c2ca 100644 --- a/apps/docs/pages/guides/auth/auth-helpers/nextjs-server-components.mdx +++ b/apps/docs/pages/guides/auth/auth-helpers/nextjs-server-components.mdx @@ -220,11 +220,13 @@ export default function SupabaseListener({ accessToken }) { const router = useRouter() useEffect(() => { - supabase.auth.onAuthStateChange((event, session) => { + const { data: subscription } = supabase.auth.onAuthStateChange((event, session) => { if (session?.access_token !== accessToken) { router.refresh() } }) + + return () => subscription.unsubscribe() }, [accessToken]) return null diff --git a/apps/docs/pages/guides/auth/auth-helpers/sveltekit.mdx b/apps/docs/pages/guides/auth/auth-helpers/sveltekit.mdx index e6d1d141100..bcdcef28fc2 100644 --- a/apps/docs/pages/guides/auth/auth-helpers/sveltekit.mdx +++ b/apps/docs/pages/guides/auth/auth-helpers/sveltekit.mdx @@ -39,7 +39,7 @@ yarn add @supabase/auth-helpers-sveltekit ### Configuration -Set up the fillowing env vars. For local development you can set them in a `.env` file. See an [example](https://github.com/supabase/auth-helpers/blob/main/examples/sveltekit/.env.example). +Set up the following env vars. For local development you can set them in a `.env` file. See an [example](https://github.com/supabase/auth-helpers/blob/main/examples/sveltekit/.env.example). ```bash # Find these in your Supabase project settings > API diff --git a/apps/docs/pages/guides/auth/overview.mdx b/apps/docs/pages/guides/auth/overview.mdx index 97044096e13..d46b0eb3e9d 100644 --- a/apps/docs/pages/guides/auth/overview.mdx +++ b/apps/docs/pages/guides/auth/overview.mdx @@ -1,5 +1,5 @@ import Layout from '~/layouts/DefaultGuideLayout' -import NewContent from '~/pages/new/auth.mdx' + import { useRouter } from 'next/router' export const meta = { @@ -174,12 +174,6 @@ When users sign up, Supabase assigns them a unique ID. You can reference this ID -export const Page = ({ children }) => { - const router = useRouter() - if (process.env.NEXT_PUBLIC_NEW_DOCS === 'true' && !router.asPath.includes('overview')) { - return NewContent() - } - return -} +export const Page = ({ children }) => export default Page diff --git a/apps/docs/pages/guides/database/full-text-search.mdx b/apps/docs/pages/guides/database/full-text-search.mdx index 51de6e5bdaa..c3fa7beaa92 100644 --- a/apps/docs/pages/guides/database/full-text-search.mdx +++ b/apps/docs/pages/guides/database/full-text-search.mdx @@ -108,8 +108,7 @@ const { data, error } = await supabase.from('books').select().eq('title', 'Harry final result = await client .from('books') .select() - .eq('title', 'Harry') - .execute(); + .eq('title', 'Harry'); ``` @@ -146,8 +145,7 @@ const { data, error } = await supabase.from('books').select().textSearch('title' final result = await client .from('books') .select() - .textSearch('title', "'Harry'") - .execute(); + .textSearch('title', "'Harry'"); ``` @@ -191,8 +189,7 @@ const { data, error } = await supabase.from('books').select().textSearch('descri final result = await client .from('books') .select() - .textSearch('description', "'big'") - .execute(); + .textSearch('description', "'big'"); ``` @@ -278,8 +275,7 @@ const { data, error } = await supabase final result = await client .from('books') .select() - .textSearch('description', "'little' & 'big'") - .execute(); + .textSearch('description', "'little' & 'big'"); ``` @@ -331,8 +327,7 @@ const { data, error } = await supabase final result = await client .from('books') .select() - .textSearch('description', "'little' | 'big'") - .execute(); + .textSearch('description', "'little' | 'big'"); ``` @@ -430,8 +425,7 @@ const { data, error } = await supabase.from('books').select().textSearch('fts', final result = await client .from('books') .select() - .textSearch('fts', "'little' & 'big'") - .execute(); + .textSearch('fts', "'little' & 'big'"); ``` @@ -488,8 +482,7 @@ const { data, error } = await supabase final result = await client .from('books') .select() - .textSearch('description', "'big' <-> 'dreams'") - .execute(); + .textSearch('description', "'big' <-> 'dreams'"); ``` @@ -531,8 +524,7 @@ const { data, error } = await supabase final result = await client .from('books') .select() - .textSearch('description', "'year' <2> 'school'") - .execute(); + .textSearch('description', "'year' <2> 'school'"); ``` @@ -577,8 +569,7 @@ const { data, error } = await supabase final result = await client .from('books') .select() - .textSearch('description', "'big' & !'little'") - .execute(); + .textSearch('description', "'big' & !'little'"); ``` diff --git a/apps/docs/pages/guides/database/functions.mdx b/apps/docs/pages/guides/database/functions.mdx index 0204ef78eac..3c547cf93fd 100644 --- a/apps/docs/pages/guides/database/functions.mdx +++ b/apps/docs/pages/guides/database/functions.mdx @@ -88,9 +88,8 @@ Reference: [rpc()](../../reference/javascript/rpc) ```dart -final res = await supabase - .rpc('hello_world') - .execute(); +final data = await supabase + .rpc('hello_world'); ``` Reference: [rpc()](../../reference/dart/rpc) @@ -199,10 +198,9 @@ const { data, error } = supabase.rpc('get_planets').eq('id', 1) ```dart -final res = await supabase +final data = await supabase .rpc('get_planets') - .eq('id', 1) - .execute(); + .eq('id', 1); ``` @@ -254,9 +252,8 @@ const { data, error } = await supabase.rpc('add_planet', { name: 'Jakku' }) ```dart -final res = await supabase - .rpc('add_planet', params: { 'name': 'Jakku' }) - .execute(); +final data = await supabase + .rpc('add_planet', params: { 'name': 'Jakku' }); ``` diff --git a/apps/docs/pages/guides/database/tables.mdx b/apps/docs/pages/guides/database/tables.mdx index 78d1b62115a..3a8caa24c19 100644 --- a/apps/docs/pages/guides/database/tables.mdx +++ b/apps/docs/pages/guides/database/tables.mdx @@ -206,7 +206,7 @@ const { data, error } = await supabase ```sql -final res = await supabase +await supabase .from('movies') .insert([{ name: 'The Empire Strikes Back', @@ -214,7 +214,7 @@ final res = await supabase }, { name: 'Return of the Jedi', description: 'After a daring mission to rescue Han Solo from Jabba the Hutt, the Rebels dispatch to Endor to destroy the second Death Star.' - }]).execute(); + }]); ``` diff --git a/apps/docs/pages/guides/functions.mdx b/apps/docs/pages/guides/functions.mdx index 9c970cb6bfe..a382f7ccde1 100644 --- a/apps/docs/pages/guides/functions.mdx +++ b/apps/docs/pages/guides/functions.mdx @@ -1,6 +1,4 @@ import Layout from '~/layouts/DefaultGuideLayout' -import { Button, GlassPanel } from 'ui' -import Link from 'next/link' export const meta = { id: 'functions', diff --git a/apps/docs/pages/guides/getting-started.mdx b/apps/docs/pages/guides/getting-started.mdx index 5cb4efb70e2..535ee897ec6 100644 --- a/apps/docs/pages/guides/getting-started.mdx +++ b/apps/docs/pages/guides/getting-started.mdx @@ -85,28 +85,28 @@ export const webapps = [ title: 'Angular', href: '/guides/getting-started/tutorials/with-angular', description: - 'Learn how to build a user management app with Nuxt 3 and Supabase Database, Auth, and Storage functionality.', + 'Learn how to build a user management app with Angular and Supabase Database, Auth, and Storage functionality.', icon: '/docs/img/icons/angular-icon', }, { title: 'RedwoodJS', href: '/guides/getting-started/tutorials/with-redwoodjs', description: - 'Learn how to build a user management app with Nuxt 3 and Supabase Database, Auth, and Storage functionality.', + 'Learn how to build a user management app with RedwoodJS and Supabase Database, Auth, and Storage functionality.', icon: '/docs/img/icons/redwood-icon', }, { title: 'Svelte', href: '/guides/getting-started/tutorials/with-svelte', description: - 'Learn how to build a user management app with Nuxt 3 and Supabase Database, Auth, and Storage functionality.', + 'Learn how to build a user management app with Svelte and Supabase Database, Auth, and Storage functionality.', icon: '/docs/img/icons/svelte-icon', }, { title: 'SvelteKit', href: '/guides/getting-started/tutorials/with-sveltekit', description: - 'Learn how to build a user management app with Nuxt 3 and Supabase Database, Auth, and Storage functionality.', + 'Learn how to build a user management app with SvelteKit and Supabase Database, Auth, and Storage functionality.', icon: '/docs/img/icons/svelte-icon', }, { @@ -120,28 +120,28 @@ export const webapps = [ title: 'Expo', href: '/guides/getting-started/tutorials/with-expo', description: - 'Learn how to build a user management app with React and Supabase Database, Auth, and Storage functionality.', + 'Learn how to build a user management app with Expo and Supabase Database, Auth, and Storage functionality.', icon: '/docs/img/icons/expo-icon', }, { title: 'Ionic React', href: '/guides/getting-started/tutorials/with-ionic-react', description: - 'Learn how to build a user management app with React and Supabase Database, Auth, and Storage functionality.', + 'Learn how to build a user management app with Ionic React and Supabase Database, Auth, and Storage functionality.', icon: '/docs/img/icons/ionic-icon', }, { title: 'Ionic Vue', href: '/guides/getting-started/tutorials/with-ionic-vue', description: - 'Learn how to build a user management app with Flutter and Supabase Database, Auth, and Storage functionality.', + 'Learn how to build a user management app with Ionic Vue and Supabase Database, Auth, and Storage functionality.', icon: '/docs/img/icons/ionic-icon', }, { title: 'Ionic Angular', href: '/guides/getting-started/tutorials/with-ionic-angular', description: - 'Learn how to build a user management app with NextJS and Supabase Database, Auth, and Storage functionality.', + 'Learn how to build a user management app with Ionic Angular and Supabase Database, Auth, and Storage functionality.', icon: '/docs/img/icons/ionic-icon', }, ] diff --git a/apps/docs/pages/guides/integrations/picket.mdx b/apps/docs/pages/guides/integrations/picket.mdx new file mode 100644 index 00000000000..d33fa8f7988 --- /dev/null +++ b/apps/docs/pages/guides/integrations/picket.mdx @@ -0,0 +1,572 @@ +import Layout from '~/layouts/DefaultGuideLayout' + +export const meta = { + id: 'picket', + title: 'Picket', + description: + "Get the best of web2 and web3. Picket allows your users to log in with their wallet without sacrificing Supabase's awesome data management and security features.", +} + +[Picket](https://picketapi.com) is a developer-first, multi-chain web3 auth platform. With Picket, you can easily authenticate users via their wallets and token gate anything. + +This guide steps through building a simple todo list Next.js application with Picket and Supabase. We use Picket to allow users to login into our app with their wallets and leverage Supabase's Row Level Security (RLS) to securely store user information off-chain. + +> Checkout a [live demo](https://picket-supabase-auth-example-v36z.vercel.app/) of a Picket + Supabase integration + +The code for this guide is based of [this example repo](https://github.com/picketapi/picket-supabase-auth-example). + +## Requirements + +- You have [Supabase](https://supabase.com) account. If you don't, sign up at https://supabase.com/ +- You have a [Picket](https://picketapi.com) account. If you don't, sign up at https://picketapi.com/ +- You've read the [Picket Setup Guide](https://docs.picketapi.com/picket-docs/quick-start-guides/quick-start-guides/start-here-setup) +- Familiarity with [React](https://reactjs.org/) and [Next.js](https://nextjs.org/) + +## Step 1: Create a Picket Project + +First, we'll create a new project in our [Picket dashboard](https://picketapi.com/dashboard). + +Click the `Create New Project` button at the top of the Projects section on your [Picket dashboard](https://picketapi.com/dashboard). Edit the project to give it a memorable name. + +![Picket project settings](/docs/img/guides/integrations/picket/picket_project.png) + +We're done for now! We'll revisit this project when we are setting up environment variables in our app. + +## Step 2: Create a Supabase Project + +From your [Supabase dashboard](https://app.supabase.com/), click `New project`. + +Enter a `Name` for your Supabase project. + +Enter a secure `Database Password`. + +Select the any `Region`. + +Click `Create new project`. + +![Supabase project settings](/docs/img/guides/integrations/picket/supabase_project.png) + +## Step 3: Create new New Table with RLS in Supabase + +### Create a `todos` Table + +From the sidebar menu in the [Supabase dashboard](https://app.supabase.com/), click `Table editor`, then `New table`. + +Enter `todos` as the `Name` field. + +Select `Enable Row Level Security (RLS)`. + +Create four columns: + +- `name` as `text` +- `wallet_address` as `text` +- `completed` as `bool` with the default value `false` +- `created_at` as timestamptz with a default value of `now()` + +Click `Save` to create the new table. + +![todos table](/docs/img/guides/integrations/picket/new_table.png) + +### Setup Row Level Security (RLS) + +Now we want to make sure that only the `todos` owner, the user's `wallet_address`, can access their todos. The key component of the this RLS policy is the expression + +```sql +((jwt() ->> 'walletAddress'::text) = wallet_address) +``` + +This expression checks that the wallet address in the requesting JWT access token is the same as the `wallet_address` in the `todos` table. + +![RLS policy](/docs/img/guides/integrations/picket/rls_policy.png) + +## Step 4: Create a Next.js app + +Now, let's start building! + +Create a [new Typescript Next.js app](https://nextjs.org/docs/getting-started) + +```bash +npx create-next-app@latest --typescript +``` + +Create a `.env.local` file and enter the following values + +- `NEXT_PUBLIC_PICKET_PUBLISHABLE_KEY` => Copy the publishable key from the Picket project you created in step 1 +- `PICKET_PROJECT_SECRET_KEY` => Copy the secret key from the Picket project you created in the step 1 +- `NEXT_PUBLIC_SUPABASE_URL` => You can find this URL under "Settings > API" in your Supabase project +- `NEXT_PUBLIC_SUPABASE_ANON_KEY` => You can find this project API key under "Settings > API" in your Supabase project +- `SUAPBASE_JWT_SECRET`=> You can find this secret under "Settings > API" in your Supabase project + +```txt +NEXT_PUBLIC_PICKET_PUBLISHABLE_KEY="YOUR_PICKET_PUBLISHABLE_KEY" +PICKET_PROJECT_SECRET_KEY="YOUR_PICKET_PROJECT_SECRET_KEY" +NEXT_PUBLIC_SUPABASE_URL="YOUR_SUPABASE_URL" +NEXT_PUBLIC_SUPABASE_ANON_KEY="YOUR_SUPABASE_ANON_KEY" +SUPABASE_JWT_SECRET="YOUR_SUPABASE_JWT_SECRET" +``` + +## Step 5: Setup Picket for Wallet Login + +> For more information on how to setup [Picket](https://picketapi.com) in your Next.js app, checkout the [Picket getting started guide](https://docs.picketapi.com/picket-docs/quick-start-guides/quick-start-guides/wallet-login) +> After initializing our app, we can setup Picket. + +Install the Picket [React](/picket-docs/reference/libraries-and-sdks/react-sdk-picket-react) and [Node](/picket-docs/reference/libraries-and-sdks/node.js-library-picket-node) libraries + +```bash +npm i @picketapi/picket-react @picketapi/picket-node +``` + +Update `pages/_app.tsx` to setup the `PicketProvider` + +```tsx +import '../styles/globals.css' +import type { AppProps } from 'next/app' + +import { PicketProvider } from '@picketapi/picket-react' + +export default function App({ Component, pageProps }: AppProps) { + return ( + + + + ) +} +``` + +Update `pages/index.tsx` to let users log in and out with their wallet + +```tsx +import { GetServerSideProps } from 'next' +import { useRouter } from 'next/router' +import { useCallback } from 'react' + +import styles from '../styles/Home.module.css' + +import { usePicket } from '@picketapi/picket-react' +import { cookieName } from '../utils/supabase' + +type Props = { + loggedIn: boolean +} + +export default function Home(props: Props) { + const { loggedIn } = props + const { login, logout, authState } = usePicket() + const router = useRouter() + + const handleLogin = useCallback(async () => { + let auth = authState + // no need to re-login if they've already connected with Picket + if (!auth) { + // login with Picket + auth = await login() + } + + // login failed + if (!auth) return + + // create a corresponding supabase access token + await fetch('/api/login', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + accessToken: auth.accessToken, + }), + }) + // redirect to their todos page + router.push('/todos') + }, [authState, login, router]) + + const handleLogout = useCallback(async () => { + // clear both picket and supabase session + await logout() + await fetch('/api/logout', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + }) + // refresh the page + router.push('/') + }, [logout, router]) + + return ( +
+
+ {loggedIn ? ( + + ) : ( + + )} +
+
+ ) +} + +export const getServerSideProps: GetServerSideProps = async ({ req }) => { + // get supabase token server-side + const accessToken = req.cookies[cookieName] + + if (!accessToken) { + return { + props: { + loggedIn: false, + }, + } + } + + return { + props: { + loggedIn: true, + }, + } +} +``` + +## Step 6: Issue a Supabase JWT on Wallet Login + +Great, now we have setup a typical Picket Next.js app. Next, we need to implement the log in/out API routes to allow users to securely query our Supabase project. + +First, install dependencies + +```bash +npm install @supabase/supabase-js jsonwebtoken cookie js-cookie +``` + +Create a utility function to create a Supabase client with a custom access token in `utils/supabase.ts` + +```ts +import { createClient, SupabaseClientOptions } from '@supabase/supabase-js' + +export const cookieName = 'sb-access-token' + +const getSupabase = (accessToken: string) => { + const options: SupabaseClientOptions<'public'> = {} + + if (accessToken) { + options.global = { + headers: { + // This gives Supabase information about the user (wallet) making the request + Authorization: `Bearer ${accessToken}`, + }, + } + } + + const supabase = createClient( + process.env.NEXT_PUBLIC_SUPABASE_URL!, + process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!, + options + ) + + return supabase +} + +export { getSupabase } +``` + +Create new api route `pages/api/login.ts`. This route validates the Picket access token then issues another equivalent Supabase access token for us to use with the Supabase client. + +```ts +import type { NextApiRequest, NextApiResponse } from 'next' +import jwt from 'jsonwebtoken' +import cookie from 'cookie' +import Picket from '@picketapi/picket-node' + +import { cookieName } from '../../utils/supabase' + +// create picket node client with your picket secret api key +const picket = new Picket(process.env.PICKET_PROJECT_SECRET_KEY!) + +const expToExpiresIn = (exp: number) => exp - Math.floor(Date.now() / 1000) + +export default async function handler(req: NextApiRequest, res: NextApiResponse) { + const { accessToken } = req.body + // omit expiration time,.it will conflict with jwt.sign + const { exp, ...payload } = await picket.validate(accessToken) + const expiresIn = expToExpiresIn(exp) + + const supabaseJWT = jwt.sign( + { + ...payload, + }, + process.env.SUPABASE_JWT_SECRET!, + { + expiresIn, + } + ) + + // Set a new cookie with the name + res.setHeader( + 'Set-Cookie', + cookie.serialize(cookieName, supabaseJWT, { + path: '/', + secure: process.env.NODE_ENV !== 'development', + // allow the cookie to be accessed client-side + httpOnly: false, + sameSite: 'strict', + maxAge: expiresIn, + }) + ) + res.status(200).json({}) +} +``` + +And now create an equivalent logout api route `/pages/api/logout.ts` to delete the Supabase access token cookie. + +```ts +import type { NextApiRequest, NextApiResponse } from 'next' +import cookie from 'cookie' + +import { cookieName } from '../../utils/supabase' + +export default async function handler(_req: NextApiRequest, res: NextApiResponse) { + // Clear the supabase cookie + res.setHeader( + 'Set-Cookie', + cookie.serialize(cookieName, '', { + path: '/', + maxAge: -1, + }) + ) + + res.status(200).json({}) +} +``` + +We can now login and logout to the app with our wallet! + +## Step 7: Interacting with Data in Supabase + +Now that we can login to the app, it's time to start interacting with Supabase. Let's make a todo list page for authenticated users. + +Create a new file `pages/todos.tsx` + +```tsx +import { GetServerSideProps } from 'next' +import Head from 'next/head' +import Link from 'next/link' +import { useState, useMemo } from 'react' +import jwt from 'jsonwebtoken' +import Cookies from 'js-cookie' + +import styles from '../styles/Home.module.css' + +import { getSupabase, cookieName } from '../utils/supabase' + +type Todo = { + name: string + completed: boolean +} + +type Props = { + walletAddress: string + todos: Todo[] +} + +const displayWalletAddress = (walletAddress: string) => + `${walletAddress.slice(0, 6)}...${walletAddress.slice(-4)}` + +export default function Todos(props: Props) { + const { walletAddress } = props + const [todos, setTodos] = useState(props.todos) + + // avoid re-creating supabase client every render + const supabase = useMemo(() => { + const accessToken = Cookies.get(cookieName) + return getSupabase(accessToken || '') + }, []) + + return ( +
+ + Picket 💜 Supabase + + +
+

Your Personal Todo List

+
+

Welcome {displayWalletAddress(walletAddress)},

+

+ Your todo list is stored in Supabase and are only accessible to you and your wallet + address. Picket + Supabase makes it easy to build scalable, hybrid web2 and web3 apps. + Use Supabase to store non-critical or private data off-chain like user app preferences + or todo lists. +

+
+
+

Todo List

+ {todos.map((todo) => ( +
+ { + await supabase.from('todos').upsert({ + wallet_address: walletAddress, + name: todo.name, + completed: !todo.completed, + }) + setTodos((todos) => + todos.map((t) => (t.name === todo.name ? { ...t, completed: !t.completed } : t)) + ) + }} + /> + + {todo.name} + +
+ ))} +
+ + Go back home → + +
+
+
+
+ ) +} + +export const getServerSideProps: GetServerSideProps = async ({ req }) => { + // example of fetching data server-side + const accessToken = req.cookies[cookieName] + + // require authentication + if (!accessToken) { + return { + redirect: { + destination: '/', + }, + props: { + walletAddress: '', + todos: [], + }, + } + } + + // check if logged in user has completed the tutorial + const supabase = getSupabase(accessToken) + const { walletAddress } = jwt.decode(accessToken) as { + walletAddress: string + } + + // get todos for the users + // if none exist, create the default todos + let { data } = await supabase.from('todos').select('*') + + if (!data || data.length === 0) { + let error = null + ;({ data, error } = await supabase + .from('todos') + .insert([ + { + wallet_address: walletAddress, + name: 'Complete the Picket + Supabase Tutorial', + completed: true, + }, + { + wallet_address: walletAddress, + name: 'Create a Picket Account (https://picketapi.com/)', + completed: false, + }, + { + wallet_address: walletAddress, + name: 'Read the Picket Docs (https://docs.picketapi.com/)', + completed: false, + }, + { + wallet_address: walletAddress, + name: 'Build an Awesome Web3 Experience', + completed: false, + }, + ]) + .select('*')) + + if (error) { + // log error and redirect home + console.error(error) + return { + redirect: { + destination: '/', + }, + props: { + walletAddress: '', + todos: [], + }, + } + } + } + + return { + props: { + walletAddress, + todos: data as Todo[], + }, + } +} +``` + +This is a long file, but don't be intimidated. The page is actually straightforward. It + +1. Verifies server-side that the user is authenticated and if they are not redirects them to the homepage +2. Checks to see if they already have `todos` . If so, it returns them. If not, it initializes them for the users +3. We render the `todos` and when the user selects or deselects a todo, we update the data in the database + +## Step 8: Try it Out! + +And that's it. If you haven't already, run your app to test it out yourself + +```bash +# start the app +npm run dev +# open http://localhost:3000 +``` + +### What's Next? + +- Explore the [Picket documentation](https://docs.picketapi.com/picket-docs/) +- Play with the live [Picket + Supabase demo](https://picket-supabase-auth-example-v36z.vercel.app/) +- Checkout Picket's [example Github repositories](https://github.com/picketapi) + +### Common Use-Case for Picket + Supabase + +- Account linking. Allow users to associate their wallet address(es) with their existing web2 account in your app +- Leverage Supabase's awesome libraries and ecosystem while still enabling wallet login +- Store app-specific data, like user preferences, about your user's wallet adress off-chain +- Cache on-chain data to improve your DApp's performance + +export const Page = ({ children }) => + +export default Page diff --git a/apps/docs/pages/guides/resources/supabase-cli/local-development.mdx b/apps/docs/pages/guides/resources/supabase-cli/local-development.mdx index eb4d4572a8b..b0f45364b52 100644 --- a/apps/docs/pages/guides/resources/supabase-cli/local-development.mdx +++ b/apps/docs/pages/guides/resources/supabase-cli/local-development.mdx @@ -31,6 +31,13 @@ Make sure you have these installed on your local machine: supabase login ``` + + +If you installed the Supabase CLI via NPM you may have to use `npx supabase login`. + + + + ## Initialize your project Create a new folder for your project and start a new git repository: diff --git a/apps/docs/pages/guides/tutorials.mdx b/apps/docs/pages/guides/tutorials.mdx index 39f553feea8..887788bdb39 100644 --- a/apps/docs/pages/guides/tutorials.mdx +++ b/apps/docs/pages/guides/tutorials.mdx @@ -84,31 +84,31 @@ export const webapps = [ icon: '/docs/img/icons/nuxt-icon', }, { - title: 'Anglular', + title: 'Angular', href: '/guides/tutorials/with-angular', description: - 'Learn how to build a user management app with Nuxt 3 and Supabase Database, Auth, and Storage functionality.', + 'Learn how to build a user management app with Angular and Supabase Database, Auth, and Storage functionality.', icon: '/docs/img/icons/angular-icon', }, { title: 'RedwoodJS', href: '/guides/tutorials/with-redwoodjs', description: - 'Learn how to build a user management app with Nuxt 3 and Supabase Database, Auth, and Storage functionality.', + 'Learn how to build a user management app with RedwoodJS and Supabase Database, Auth, and Storage functionality.', icon: '/docs/img/icons/redwood-icon', }, { title: 'Svelte', href: '/guides/tutorials/with-svelte', description: - 'Learn how to build a user management app with Nuxt 3 and Supabase Database, Auth, and Storage functionality.', + 'Learn how to build a user management app with Svelte and Supabase Database, Auth, and Storage functionality.', icon: '/docs/img/icons/svelte-icon', }, { title: 'SvelteKit', href: '/guides/tutorials/with-sveltekit', description: - 'Learn how to build a user management app with Nuxt 3 and Supabase Database, Auth, and Storage functionality.', + 'Learn how to build a user management app with SvelteKit and Supabase Database, Auth, and Storage functionality.', icon: '/docs/img/icons/svelte-icon', }, ] @@ -125,7 +125,7 @@ export const mobile = [ title: 'Expo', href: '/guides/tutorials/with-expo', description: - 'Learn how to build a user management app with React and Supabase Database, Auth, and Storage functionality.', + 'Learn how to build a user management app with Expo and Supabase Database, Auth, and Storage functionality.', icon: '/docs/img/icons/expo-icon', }, ] @@ -135,21 +135,21 @@ export const lowCode = [ title: 'Ionic React', href: '/guides/tutorials/with-ionic-react', description: - 'Learn how to build a user management app with React and Supabase Database, Auth, and Storage functionality.', + 'Learn how to build a user management app with Ionic React and Supabase Database, Auth, and Storage functionality.', icon: '/docs/img/icons/ionic-icon', }, { title: 'Ionic Vue', href: '/guides/tutorials/with-ionic-vue', description: - 'Learn how to build a user management app with Flutter and Supabase Database, Auth, and Storage functionality.', + 'Learn how to build a user management app with Ionic Vue and Supabase Database, Auth, and Storage functionality.', icon: '/docs/img/icons/ionic-icon', }, { title: 'Ionic Angular', href: '/guides/tutorials/with-ionic-angular', description: - 'Learn how to build a user management app with NextJS and Supabase Database, Auth, and Storage functionality.', + 'Learn how to build a user management app with Ionic Angular and Supabase Database, Auth, and Storage functionality.', icon: '/docs/img/icons/ionic-icon', }, ] diff --git a/apps/docs/pages/reference/api/[...slug].tsx b/apps/docs/pages/reference/api/[...slug].tsx index 58d617d5cac..382c882a190 100644 --- a/apps/docs/pages/reference/api/[...slug].tsx +++ b/apps/docs/pages/reference/api/[...slug].tsx @@ -15,8 +15,8 @@ export default function Config(props) { return } -export async function getStaticProps({ params }: { params: { slug: string[] } }) { - return handleRefStaticProps(sections, params, '/api', '/api') +export async function getStaticProps() { + return handleRefStaticProps(sections, '/api') } export function getStaticPaths() { diff --git a/apps/docs/pages/reference/cli/[...slug].tsx b/apps/docs/pages/reference/cli/[...slug].tsx index 2e4a2218b3e..3555095ee72 100644 --- a/apps/docs/pages/reference/cli/[...slug].tsx +++ b/apps/docs/pages/reference/cli/[...slug].tsx @@ -14,8 +14,8 @@ export default function CliRef(props) { return } -export async function getStaticProps({ params }: { params: { slug: string[] } }) { - return handleRefStaticProps(sections, params, '/cli', '/cli') +export async function getStaticProps() { + return handleRefStaticProps(sections, '/cli') } export function getStaticPaths() { diff --git a/apps/docs/pages/reference/dart/[...slug].tsx b/apps/docs/pages/reference/dart/[...slug].tsx index 4fecc97170d..f35f74c6120 100644 --- a/apps/docs/pages/reference/dart/[...slug].tsx +++ b/apps/docs/pages/reference/dart/[...slug].tsx @@ -9,11 +9,11 @@ import handleRefStaticProps from '~/lib/mdx/handleRefStaticProps' const sections = flattenSections(clientLibsCommonSections) export default function JSReference(props) { - return + return } -export async function getStaticProps({ params }: { params: { slug: string[] } }) { - return handleRefStaticProps(sections, params, '/dart', '/dart') +export async function getStaticProps() { + return handleRefStaticProps(sections, '/dart') } export function getStaticPaths() { diff --git a/apps/docs/pages/reference/dart/crawlers/[...slug].tsx b/apps/docs/pages/reference/dart/crawlers/[...slug].tsx index c12f3cabbf2..036933a1682 100644 --- a/apps/docs/pages/reference/dart/crawlers/[...slug].tsx +++ b/apps/docs/pages/reference/dart/crawlers/[...slug].tsx @@ -29,13 +29,14 @@ export default function JSReference(props) { spec={spec} typeSpec={typeSpec} pageProps={props} + type="client-lib" /> ) } -export async function getStaticProps({ params }: { params: { slug: string[] } }) { - return handleRefStaticProps(sections, params, '/dart', '/dart') +export async function getStaticProps() { + return handleRefStaticProps(sections, '/dart') } export function getStaticPaths() { diff --git a/apps/docs/pages/reference/dart/v0/[...slug].tsx b/apps/docs/pages/reference/dart/v0/[...slug].tsx index f0daf4e9d6e..4b6a485d9c6 100644 --- a/apps/docs/pages/reference/dart/v0/[...slug].tsx +++ b/apps/docs/pages/reference/dart/v0/[...slug].tsx @@ -9,11 +9,11 @@ import handleRefStaticProps from '~/lib/mdx/handleRefStaticProps' const sections = flattenSections(clientLibsCommonSections) export default function JSReference(props) { - return + return } -export async function getStaticProps({ params }: { params: { slug: string[] } }) { - return handleRefStaticProps(sections, params, '/dart/v0', '/dart/v0') +export async function getStaticProps() { + return handleRefStaticProps(sections, '/dart/v0') } export function getStaticPaths() { diff --git a/apps/docs/pages/reference/dart/v0/crawlers/[...slug].tsx b/apps/docs/pages/reference/dart/v0/crawlers/[...slug].tsx index 2e67a5b5c8e..8539659431d 100644 --- a/apps/docs/pages/reference/dart/v0/crawlers/[...slug].tsx +++ b/apps/docs/pages/reference/dart/v0/crawlers/[...slug].tsx @@ -28,13 +28,14 @@ export default function JSReference(props) { spec={spec} typeSpec={typeSpec} pageProps={props} + type="client-lib" /> ) } -export async function getStaticProps({ params }: { params: { slug: string[] } }) { - return handleRefStaticProps(sections, params, '/dart/v0', '/dart/v0') +export async function getStaticProps() { + return handleRefStaticProps(sections, '/dart/v0') } export function getStaticPaths() { diff --git a/apps/docs/pages/reference/javascript/[...slug].tsx b/apps/docs/pages/reference/javascript/[...slug].tsx index cab58348221..3cbbcc08694 100644 --- a/apps/docs/pages/reference/javascript/[...slug].tsx +++ b/apps/docs/pages/reference/javascript/[...slug].tsx @@ -10,11 +10,19 @@ import handleRefStaticProps from '~/lib/mdx/handleRefStaticProps' const sections = flattenSections(clientLibsCommonSections) export default function JSReference(props) { - return + return ( + + ) } -export async function getStaticProps({ params }: { params: { slug: string[] } }) { - return handleRefStaticProps(sections, params, '/javascript', '/javascript') +export async function getStaticProps() { + return handleRefStaticProps(sections, '/javascript') } export function getStaticPaths() { diff --git a/apps/docs/pages/reference/javascript/crawlers/[...slug].tsx b/apps/docs/pages/reference/javascript/crawlers/[...slug].tsx index 2e6e0b271a9..67c72c5151c 100644 --- a/apps/docs/pages/reference/javascript/crawlers/[...slug].tsx +++ b/apps/docs/pages/reference/javascript/crawlers/[...slug].tsx @@ -29,13 +29,14 @@ export default function JSReference(props) { spec={spec} typeSpec={typeSpec} pageProps={props} + type="client-lib" /> ) } -export async function getStaticProps({ params }: { params: { slug: string[] } }) { - return handleRefStaticProps(sections, params, '/javascript', '/javascript') +export async function getStaticProps() { + return handleRefStaticProps(sections, '/javascript') } export function getStaticPaths() { diff --git a/apps/docs/pages/reference/javascript/v1/[...slug].tsx b/apps/docs/pages/reference/javascript/v1/[...slug].tsx index d54c9df6d94..4dd32c156ca 100644 --- a/apps/docs/pages/reference/javascript/v1/[...slug].tsx +++ b/apps/docs/pages/reference/javascript/v1/[...slug].tsx @@ -10,11 +10,19 @@ import handleRefStaticProps from '~/lib/mdx/handleRefStaticProps' const sections = flattenSections(clientLibsCommonSections) export default function JSReference(props) { - return + return ( + + ) } -export async function getStaticProps({ params }: { params: { slug: string[] } }) { - return handleRefStaticProps(sections, params, '/javascript/v1', '/javascript/v1') +export async function getStaticProps() { + return handleRefStaticProps(sections, '/javascript/v1') } export function getStaticPaths() { diff --git a/apps/docs/pages/reference/javascript/v1/crawlers/[...slug].tsx b/apps/docs/pages/reference/javascript/v1/crawlers/[...slug].tsx index bbb38f4598a..18c498125ef 100644 --- a/apps/docs/pages/reference/javascript/v1/crawlers/[...slug].tsx +++ b/apps/docs/pages/reference/javascript/v1/crawlers/[...slug].tsx @@ -28,13 +28,14 @@ export default function JSReference(props) { spec={spec} typeSpec={typeSpec} pageProps={props} + type="client-lib" /> ) } -export async function getStaticProps({ params }: { params: { slug: string[] } }) { - return handleRefStaticProps(sections, params, '/javascript/v1', '/javascript/v1') +export async function getStaticProps() { + return handleRefStaticProps(sections, '/javascript/v1') } export function getStaticPaths() { diff --git a/apps/docs/pages/reference/self-hosting-auth/[...slug].tsx b/apps/docs/pages/reference/self-hosting-auth/[...slug].tsx index 819fd773892..c7b2382af16 100644 --- a/apps/docs/pages/reference/self-hosting-auth/[...slug].tsx +++ b/apps/docs/pages/reference/self-hosting-auth/[...slug].tsx @@ -12,11 +12,11 @@ const sections = flattenSections(selfHostingAuthCommonSections) const spec = gen_v3(authSpec, 'wat', { apiUrl: 'apiv0' }) export default function JSReference(props) { - return + return } -export async function getStaticProps({ params }: { params: { slug: string[] } }) { - return handleRefStaticProps(sections, params, '/self-hosting-auth', '/self-hosting-auth') +export async function getStaticProps() { + return handleRefStaticProps(sections, '/self-hosting-auth') } export function getStaticPaths() { diff --git a/apps/docs/pages/reference/self-hosting-realtime/[...slug].tsx b/apps/docs/pages/reference/self-hosting-realtime/[...slug].tsx index 240ea180f0c..49e91894a06 100644 --- a/apps/docs/pages/reference/self-hosting-realtime/[...slug].tsx +++ b/apps/docs/pages/reference/self-hosting-realtime/[...slug].tsx @@ -9,11 +9,11 @@ import { gen_v3 } from '~/lib/refGenerator/helpers' const sections = flattenSections(selfHostingRealtimeCommonSections) export default function JSReference(props) { - return + return } -export async function getStaticProps({ params }: { params: { slug: string[] } }) { - return handleRefStaticProps(sections, params, '/self-hosting-realtime', '/self-hosting-realtime') +export async function getStaticProps() { + return handleRefStaticProps(sections, '/self-hosting-realtime') } export function getStaticPaths() { diff --git a/apps/docs/pages/reference/self-hosting-storage/[...slug].tsx b/apps/docs/pages/reference/self-hosting-storage/[...slug].tsx index 2d2de874622..1b4b99fb9c8 100644 --- a/apps/docs/pages/reference/self-hosting-storage/[...slug].tsx +++ b/apps/docs/pages/reference/self-hosting-storage/[...slug].tsx @@ -12,11 +12,11 @@ const sections = flattenSections(selfHostingStorageCommonSections) const spec = gen_v3(storageSpec, 'wat', { apiUrl: 'apiv0' }) export default function JSReference(props) { - return + return } -export async function getStaticProps({ params }: { params: { slug: string[] } }) { - return handleRefStaticProps(sections, params, '/self-hosting-storage', '/self-hosting-storage') +export async function getStaticProps() { + return handleRefStaticProps(sections, '/self-hosting-storage') } export function getStaticPaths() { diff --git a/apps/docs/public/img/guides/integrations/picket/new_table.png b/apps/docs/public/img/guides/integrations/picket/new_table.png new file mode 100644 index 00000000000..72aa9b21ea2 Binary files /dev/null and b/apps/docs/public/img/guides/integrations/picket/new_table.png differ diff --git a/apps/docs/public/img/guides/integrations/picket/picket_project.png b/apps/docs/public/img/guides/integrations/picket/picket_project.png new file mode 100644 index 00000000000..6d359e9af83 Binary files /dev/null and b/apps/docs/public/img/guides/integrations/picket/picket_project.png differ diff --git a/apps/docs/public/img/guides/integrations/picket/rls_policy.png b/apps/docs/public/img/guides/integrations/picket/rls_policy.png new file mode 100644 index 00000000000..347038ebe90 Binary files /dev/null and b/apps/docs/public/img/guides/integrations/picket/rls_policy.png differ diff --git a/apps/docs/public/img/guides/integrations/picket/supabase_project.png b/apps/docs/public/img/guides/integrations/picket/supabase_project.png new file mode 100644 index 00000000000..97c441b6ef8 Binary files /dev/null and b/apps/docs/public/img/guides/integrations/picket/supabase_project.png differ diff --git a/apps/docs/public/sitemap.xml b/apps/docs/public/sitemap.xml index 38b0e06220a..2e86001d252 100644 --- a/apps/docs/public/sitemap.xml +++ b/apps/docs/public/sitemap.xml @@ -78,6 +78,12 @@ 0.5 + + https://supabase.com/docs/guides/self-hosting + weekly + 0.5 + + https://supabase.com/docs/guides/storage weekly @@ -126,6 +132,78 @@ 0.5 + + https://supabase.com/docs/guides/auth/auth-captcha + weekly + 0.5 + + + + https://supabase.com/docs/guides/auth/auth-email-templates + weekly + 0.5 + + + + https://supabase.com/docs/guides/auth/auth-email + weekly + 0.5 + + + + https://supabase.com/docs/guides/auth/auth-helpers + weekly + 0.5 + + + + https://supabase.com/docs/guides/auth/auth-magic-link + weekly + 0.5 + + + + https://supabase.com/docs/guides/auth/auth-mfa + weekly + 0.5 + + + + https://supabase.com/docs/guides/auth/managing-user-data + weekly + 0.5 + + + + https://supabase.com/docs/guides/auth/overview + weekly + 0.5 + + + + https://supabase.com/docs/guides/auth/phone-login + weekly + 0.5 + + + + https://supabase.com/docs/guides/auth/row-level-security + weekly + 0.5 + + + + https://supabase.com/docs/guides/auth/server-side-rendering + weekly + 0.5 + + + + https://supabase.com/docs/guides/auth/social-login + weekly + 0.5 + + https://supabase.com/docs/guides/database/arrays weekly @@ -216,78 +294,6 @@ 0.5 - - https://supabase.com/docs/guides/auth/auth-captcha - weekly - 0.5 - - - - https://supabase.com/docs/guides/auth/auth-email-templates - weekly - 0.5 - - - - https://supabase.com/docs/guides/auth/auth-email - weekly - 0.5 - - - - https://supabase.com/docs/guides/auth/auth-helpers - weekly - 0.5 - - - - https://supabase.com/docs/guides/auth/auth-magic-link - weekly - 0.5 - - - - https://supabase.com/docs/guides/auth/auth-mfa - weekly - 0.5 - - - - https://supabase.com/docs/guides/auth/managing-user-data - weekly - 0.5 - - - - https://supabase.com/docs/guides/auth/overview - weekly - 0.5 - - - - https://supabase.com/docs/guides/auth/phone-login - weekly - 0.5 - - - - https://supabase.com/docs/guides/auth/row-level-security - weekly - 0.5 - - - - https://supabase.com/docs/guides/auth/server-side-rendering - weekly - 0.5 - - - - https://supabase.com/docs/guides/auth/social-login - weekly - 0.5 - - https://supabase.com/docs/guides/functions/auth weekly @@ -570,12 +576,6 @@ 0.5 - - https://supabase.com/docs/guides/resources/self-hosting - weekly - 0.5 - - https://supabase.com/docs/guides/resources/supabase-cli weekly @@ -612,6 +612,12 @@ 0.5 + + https://supabase.com/docs/guides/self-hosting/docker + weekly + 0.5 + + https://supabase.com/docs/learn/auth-deep-dive/auth-deep-dive-jwts weekly @@ -642,42 +648,6 @@ 0.5 - - https://supabase.com/docs/guides/database/extensions/http - weekly - 0.5 - - - - https://supabase.com/docs/guides/database/extensions/pgcron - weekly - 0.5 - - - - https://supabase.com/docs/guides/database/extensions/pgnet - weekly - 0.5 - - - - https://supabase.com/docs/guides/database/extensions/pgtap - weekly - 0.5 - - - - https://supabase.com/docs/guides/database/extensions/plv8 - weekly - 0.5 - - - - https://supabase.com/docs/guides/database/extensions/uuid-ossp - weekly - 0.5 - - https://supabase.com/docs/guides/auth/auth-helpers/auth-ui weekly @@ -829,25 +799,43 @@ - https://supabase.com/docs/guides/functions/examples/og-image + https://supabase.com/docs/guides/database/extensions/http weekly 0.5 - https://supabase.com/docs/guides/getting-started/quickstarts/nextjs + https://supabase.com/docs/guides/database/extensions/pgcron weekly 0.5 - https://supabase.com/docs/guides/getting-started/quickstarts/nuxtjs + https://supabase.com/docs/guides/database/extensions/pgnet weekly 0.5 - https://supabase.com/docs/guides/getting-started/quickstarts/reactjs + https://supabase.com/docs/guides/database/extensions/pgtap + weekly + 0.5 + + + + https://supabase.com/docs/guides/database/extensions/plv8 + weekly + 0.5 + + + + https://supabase.com/docs/guides/database/extensions/postgis + weekly + 0.5 + + + + https://supabase.com/docs/guides/database/extensions/uuid-ossp weekly 0.5 @@ -936,6 +924,30 @@ 0.5 + + https://supabase.com/docs/guides/functions/examples/og-image + weekly + 0.5 + + + + https://supabase.com/docs/guides/getting-started/quickstarts/nextjs + weekly + 0.5 + + + + https://supabase.com/docs/guides/getting-started/quickstarts/nuxtjs + weekly + 0.5 + + + + https://supabase.com/docs/guides/getting-started/quickstarts/reactjs + weekly + 0.5 + + https://supabase.com/docs/guides/platform/sso/azure weekly @@ -954,6 +966,24 @@ 0.5 + + https://supabase.com/docs/guides/resources/supabase-cli/local-development + weekly + 0.5 + + + + https://supabase.com/docs/guides/resources/supabase-cli/managing-environments + weekly + 0.5 + + + + https://supabase.com/docs/guides/self-hosting/auth/config + weekly + 0.5 + + https://supabase.com/docs/guides/resources/migrating-to-supabase/firebase-auth weekly @@ -979,19 +1009,13 @@ - https://supabase.com/docs/guides/resources/self-hosting/docker + https://supabase.com/docs/guides/self-hosting/realtime/config weekly 0.5 - https://supabase.com/docs/guides/resources/supabase-cli/local-development - weekly - 0.5 - - - - https://supabase.com/docs/guides/resources/supabase-cli/managing-environments + https://supabase.com/docs/guides/self-hosting/storage/config weekly 0.5 @@ -3326,6 +3350,24 @@ 0.5 + + https://supabase.com/docs/reference/cli/supabase-network-bans + weekly + 0.5 + + + + https://supabase.com/docs/reference/cli/supabase-network-bans-remove + weekly + 0.5 + + + + https://supabase.com/docs/reference/cli/supabase-network-bans-get + weekly + 0.5 + + https://supabase.com/docs/reference/cli/supabase-migration weekly @@ -3488,6 +3530,12 @@ 0.5 + + https://supabase.com/docs/reference/cli/supabase-db-dump + weekly + 0.5 + + https://supabase.com/docs/reference/cli/supabase-db-diff weekly diff --git a/apps/www/data/Pricing.json b/apps/www/data/Pricing.json index 7d2dd4e24ae..dd2589b08f3 100644 --- a/apps/www/data/Pricing.json +++ b/apps/www/data/Pricing.json @@ -194,6 +194,7 @@ }, { "title": "Asset transformations", + "tooltip": "Origin images are only relevant for image transformations. Images in the bucket which are not transformed do not count towards this limit.", "tiers": { "free": false, "pro": "Unlimited transformations for 100 origin images, then 5$ per 1000 origin images", diff --git a/apps/www/lib/rss.tsx b/apps/www/lib/rss.tsx index 36fb38f5825..c0409f8393d 100644 --- a/apps/www/lib/rss.tsx +++ b/apps/www/lib/rss.tsx @@ -1,3 +1,4 @@ +import authors from 'lib/authors.json' const dayjs = require('dayjs') var utc = require('dayjs/plugin/utc') var advancedFormat = require('dayjs/plugin/advancedFormat') @@ -17,8 +18,29 @@ const generateRssItem = (post: any): string => { ` } -export const generateRss = (posts: any[]): string => { - return ` +// we generate a main rss.xml flie as well as individual files for +// authors who publish under the `planetpg` tag +export const generateRss = (posts: any[], authorID?: string): string => { + const authorInfo = authors.find((item) => item.author_id === authorID) + + if (authorID) { + return ` + + + 'Postgres | Supabase Blog + https://supabase.com/blog + Latest Postgres news from ${authorInfo?.author} at Supabase + en + ${dayjs(posts[0].date) + .utc() + .format('ddd, DD MMM YYYY HH:hh:mm ZZ')} + + ${posts.map(generateRssItem).join('')} + + +` + } else { + return ` Blog - Supabase @@ -33,4 +55,5 @@ export const generateRss = (posts: any[]): string => { ` + } } diff --git a/apps/www/pages/blog.tsx b/apps/www/pages/blog.tsx index 49eb7c8351f..a14bbd8feef 100644 --- a/apps/www/pages/blog.tsx +++ b/apps/www/pages/blog.tsx @@ -19,16 +19,23 @@ export async function getStaticProps() { const allPostsData = getSortedPosts('_blog', undefined, undefined, '** BLOG PAGE **') const categories = getAllCategories('_blog') const rss = generateRss(allPostsData) - // @ts-ignore - const planetPgPosts = allPostsData.filter((post) => post.tags.includes('planetpg')) - const planetPgrss = generateRss(planetPgPosts) // create a rss feed in public directory // rss feed is added via component in render return fs.writeFileSync('./public/rss.xml', rss) - // create separate feed for just posts promoted in PlanetPG - fs.writeFileSync('./public/planetpg-rss.xml', planetPgrss) + // generate a series of rss feeds for each author (for PlanetPG) + const planetPgPosts = allPostsData.filter((post: any) => post.tags.includes('planetpg')) + const planetPgAuthors = planetPgPosts.map((post: any) => post.author.split(',')) + const uniquePlanetPgAuthors = new Set([].concat(...planetPgAuthors)) + + uniquePlanetPgAuthors.forEach((author) => { + const authorPosts = planetPgPosts.filter((post: any) => post.author.includes(author)) + if (authorPosts.length > 0) { + const authorRss = generateRss(authorPosts, author) + fs.writeFileSync(`./public/planetpg-${author}-rss.xml`, authorRss) + } + }) return { props: { diff --git a/apps/www/pages/supasquad.mdx b/apps/www/pages/supasquad.mdx index 012750bedac..e305eab4b73 100644 --- a/apps/www/pages/supasquad.mdx +++ b/apps/www/pages/supasquad.mdx @@ -37,7 +37,7 @@ Spread the word on social channels and help to answer Supabase-related questions ### Builder -Build Supabase examples, blog about them, and them add to the [SupaSquad GitHub Org](https://github.com/supasquad). +Build Supabase examples, blog about them, and add them to the [SupaSquad GitHub Org](https://github.com/supasquad). ### Author diff --git a/apps/www/public/planetpg-angelico_de_los_reyes-rss.xml b/apps/www/public/planetpg-angelico_de_los_reyes-rss.xml new file mode 100644 index 00000000000..84fe8fab257 --- /dev/null +++ b/apps/www/public/planetpg-angelico_de_los_reyes-rss.xml @@ -0,0 +1,40 @@ + + + + 'Postgres | Supabase Blog + https://supabase.com/blog + Latest Postgres news from Angelico de los Reyes at Supabase + en + Thu, 15 Dec 2022 03:03:30 +0000 + + + https://supabase.com/blog/postgres-point-in-time-recovery + Point in Time Recovery is now available for Pro projects + https://supabase.com/blog/postgres-point-in-time-recovery + We're making PITR available for more projects, with a new Dashboard UI that makes it simple to use. + Thu, 15 Dec 2022 03:03:30 +0000 + + + https://supabase.com/blog/continuous-postgresql-backup-walg + Continuous PostgreSQL Backups using WAL-G + https://supabase.com/blog/continuous-postgresql-backup-walg + Have you ever wanted to restore your database's state to a particular moment in time? This post explains how, using WAL-G. + Sun, 02 Aug 2020 02:02:30 +0000 + + + https://supabase.com/blog/postgresql-templates + What are PostgreSQL Templates? + https://supabase.com/blog/postgresql-templates + What are PostgreSQL templates and what are they used for? + Thu, 09 Jul 2020 02:02:30 +0000 + + + https://supabase.com/blog/postgresql-physical-logical-backups + Physical vs Logical Backups in PostgreSQL + https://supabase.com/blog/postgresql-physical-logical-backups + What are physical and logical backups in Postgres? + Tue, 07 Jul 2020 02:02:30 +0000 + + + + diff --git a/apps/www/public/planetpg-ant_wilson-rss.xml b/apps/www/public/planetpg-ant_wilson-rss.xml new file mode 100644 index 00000000000..07cfe4056b8 --- /dev/null +++ b/apps/www/public/planetpg-ant_wilson-rss.xml @@ -0,0 +1,19 @@ + + + + 'Postgres | Supabase Blog + https://supabase.com/blog + Latest Postgres news from Ant Wilson at Supabase + en + Sat, 27 Feb 2021 03:03:30 +0000 + + + https://supabase.com/blog/cracking-postgres-interview + Cracking PostgreSQL Interview Questions + https://supabase.com/blog/cracking-postgres-interview + Understand the top PostgreSQL Interview Questions + Sat, 27 Feb 2021 03:03:30 +0000 + + + + diff --git a/apps/www/public/planetpg-burggraf-rss.xml b/apps/www/public/planetpg-burggraf-rss.xml new file mode 100644 index 00000000000..26233a86f1e --- /dev/null +++ b/apps/www/public/planetpg-burggraf-rss.xml @@ -0,0 +1,26 @@ + + + + 'Postgres | Supabase Blog + https://supabase.com/blog + Latest Postgres news from Mark Burggraf at Supabase + en + Wed, 23 Nov 2022 03:03:30 +0000 + + + https://supabase.com/blog/sql-or-nosql-both-with-postgresql + SQL or NoSQL? Why not use both (with PostgreSQL)? + https://supabase.com/blog/sql-or-nosql-both-with-postgresql + How to turn Postgres into an easy-to-use NoSQL database that retains all the power of SQL + Wed, 23 Nov 2022 03:03:30 +0000 + + + https://supabase.com/blog/postgres-wasm + Postgres WASM by Snaplet and Supabase + https://supabase.com/blog/postgres-wasm + We're open sourcing postgres-wasm, a PostgresQL server that runs inside a browser, with our friends at Snaplet. + Sun, 02 Oct 2022 02:02:30 +0000 + + + + diff --git a/apps/www/public/planetpg-michel-rss.xml b/apps/www/public/planetpg-michel-rss.xml new file mode 100644 index 00000000000..3bcd14a76b8 --- /dev/null +++ b/apps/www/public/planetpg-michel-rss.xml @@ -0,0 +1,26 @@ + + + + 'Postgres | Supabase Blog + https://supabase.com/blog + Latest Postgres news from Michel Pelletier at Supabase + en + Thu, 15 Dec 2022 03:03:30 +0000 + + + https://supabase.com/blog/vault-now-in-beta + Supabase Vault is now in Beta + https://supabase.com/blog/vault-now-in-beta + A Postgres extension to store encrypted secrets and encrypt data. + Thu, 15 Dec 2022 03:03:30 +0000 + + + https://supabase.com/blog/transparent-column-encryption-with-postgres + Transparent Column Encryption with Postgres + https://supabase.com/blog/transparent-column-encryption-with-postgres + Using pgsodium's Transparent Column Encryption to encrypt data and provide your users with row-level encryption. + Wed, 30 Nov 2022 03:03:30 +0000 + + + + diff --git a/apps/www/public/planetpg-oli_rice-rss.xml b/apps/www/public/planetpg-oli_rice-rss.xml new file mode 100644 index 00000000000..77f01e50bdf --- /dev/null +++ b/apps/www/public/planetpg-oli_rice-rss.xml @@ -0,0 +1,26 @@ + + + + 'Postgres | Supabase Blog + https://supabase.com/blog + Latest Postgres news from Oliver Rice at Supabase + en + Thu, 15 Dec 2022 03:03:30 +0000 + + + https://supabase.com/blog/pg-graphql-v1 + pg_graphql v1.0 + https://supabase.com/blog/pg-graphql-v1 + Announcing the v1.0 release of pg_graphql + Thu, 15 Dec 2022 03:03:30 +0000 + + + https://supabase.com/blog/audit + Postgres Auditing in 150 lines of SQL + https://supabase.com/blog/audit + PostgreSQL has a robust set of features which we can leverage to create a generic auditing solution in 150 lines of SQL. + Mon, 07 Mar 2022 03:03:30 +0000 + + + + diff --git a/apps/www/public/planetpg-paul_copplestone-rss.xml b/apps/www/public/planetpg-paul_copplestone-rss.xml new file mode 100644 index 00000000000..bc3d31ea3d3 --- /dev/null +++ b/apps/www/public/planetpg-paul_copplestone-rss.xml @@ -0,0 +1,33 @@ + + + + 'Postgres | Supabase Blog + https://supabase.com/blog + Latest Postgres news from Paul Copplestone at Supabase + en + Mon, 27 Jun 2022 02:02:30 +0000 + + + https://supabase.com/blog/partial-postgresql-data-dumps-with-rls + Partial data dumps using Postgres Row Level Security + https://supabase.com/blog/partial-postgresql-data-dumps-with-rls + Using RLS to create seed files for local PostgreSQL testing. + Mon, 27 Jun 2022 02:02:30 +0000 + + + https://supabase.com/blog/postgres-as-a-cron-server + Postgres as a CRON Server + https://supabase.com/blog/postgres-as-a-cron-server + Running repetitive tasks with your Postgres database. + Fri, 05 Mar 2021 03:03:30 +0000 + + + https://supabase.com/blog/postgresql-views + Postgres Views + https://supabase.com/blog/postgresql-views + Creating and using a view in PostgreSQL. + Wed, 18 Nov 2020 03:03:30 +0000 + + + + diff --git a/apps/www/public/planetpg-pavel-rss.xml b/apps/www/public/planetpg-pavel-rss.xml new file mode 100644 index 00000000000..e6dbbb4c1fe --- /dev/null +++ b/apps/www/public/planetpg-pavel-rss.xml @@ -0,0 +1,26 @@ + + + + 'Postgres | Supabase Blog + https://supabase.com/blog + Latest Postgres news from Pavel Borisov at Supabase + en + Thu, 15 Dec 2022 03:03:30 +0000 + + + https://supabase.com/blog/new-in-postgres-15 + What's new in Postgres 15? + https://supabase.com/blog/new-in-postgres-15 + Describes the release of Postgres 15, new features and reasons to use it + Thu, 15 Dec 2022 03:03:30 +0000 + + + https://supabase.com/blog/postgresql-commitfest + What is PostgreSQL commitfest and how to contribute + https://supabase.com/blog/postgresql-commitfest + A time-tested method for contributing to the core Postgres code + Wed, 26 Oct 2022 02:02:30 +0000 + + + + diff --git a/apps/www/public/planetpg-rss.xml b/apps/www/public/planetpg-rss.xml deleted file mode 100644 index 9a980d87cea..00000000000 --- a/apps/www/public/planetpg-rss.xml +++ /dev/null @@ -1,159 +0,0 @@ - - - - Blog - Supabase - https://supabase.com - Latest news from Supabase - en - Thu, 15 Dec 2022 03:03:30 +0000 - - - https://supabase.com/blog/vault-now-in-beta - Supabase Vault is now in Beta - https://supabase.com/blog/vault-now-in-beta - A Postgres extension to store encrypted secrets and encrypt data. - Thu, 15 Dec 2022 03:03:30 +0000 - - - https://supabase.com/blog/postgrest-11-prerelease - PostgREST 11 pre-release - https://supabase.com/blog/postgrest-11-prerelease - Describes new features of PostgREST 11 pre-release - Thu, 15 Dec 2022 03:03:30 +0000 - - - https://supabase.com/blog/postgres-point-in-time-recovery - Point in Time Recovery is now available for Pro projects - https://supabase.com/blog/postgres-point-in-time-recovery - We're making PITR available for more projects, with a new Dashboard UI that makes it simple to use. - Thu, 15 Dec 2022 03:03:30 +0000 - - - https://supabase.com/blog/pg-graphql-v1 - pg_graphql v1.0 - https://supabase.com/blog/pg-graphql-v1 - Announcing the v1.0 release of pg_graphql - Thu, 15 Dec 2022 03:03:30 +0000 - - - https://supabase.com/blog/new-in-postgres-15 - What's new in Postgres 15? - https://supabase.com/blog/new-in-postgres-15 - Describes the release of Postgres 15, new features and reasons to use it - Thu, 15 Dec 2022 03:03:30 +0000 - - - https://supabase.com/blog/transparent-column-encryption-with-postgres - Transparent Column Encryption with Postgres - https://supabase.com/blog/transparent-column-encryption-with-postgres - Using pgsodium's Transparent Column Encryption to encrypt data and provide your users with row-level encryption. - Wed, 30 Nov 2022 03:03:30 +0000 - - - https://supabase.com/blog/sql-or-nosql-both-with-postgresql - SQL or NoSQL? Why not use both (with PostgreSQL)? - https://supabase.com/blog/sql-or-nosql-both-with-postgresql - How to turn Postgres into an easy-to-use NoSQL database that retains all the power of SQL - Wed, 23 Nov 2022 03:03:30 +0000 - - - https://supabase.com/blog/postgresql-commitfest - What is PostgreSQL commitfest and how to contribute - https://supabase.com/blog/postgresql-commitfest - A time-tested method for contributing to the core Postgres code - Wed, 26 Oct 2022 02:02:30 +0000 - - - https://supabase.com/blog/postgres-full-text-search-vs-the-rest - Postgres Full Text Search vs the rest - https://supabase.com/blog/postgres-full-text-search-vs-the-rest - Comparing one of the most popular Postgres features against alternatives - Thu, 13 Oct 2022 02:02:30 +0000 - - - https://supabase.com/blog/postgres-wasm - Postgres WASM by Snaplet and Supabase - https://supabase.com/blog/postgres-wasm - We're open sourcing postgres-wasm, a PostgresQL server that runs inside a browser, with our friends at Snaplet. - Sun, 02 Oct 2022 02:02:30 +0000 - - - https://supabase.com/blog/choosing-a-postgres-primary-key - Choosing a Postgres Primary Key - https://supabase.com/blog/choosing-a-postgres-primary-key - Turns out the question of which identifier to use as a Primary Key is complicated -- we're going to dive into some of the complexity and inherent trade-offs, and figure things out - Wed, 07 Sep 2022 02:02:30 +0000 - - - https://supabase.com/blog/seen-by-in-postgresql - Implementing "seen by" functionality with Postgres - https://supabase.com/blog/seen-by-in-postgresql - Different approaches for tracking visitor counts with PostgreSQL. - Sun, 17 Jul 2022 02:02:30 +0000 - - - https://supabase.com/blog/partial-postgresql-data-dumps-with-rls - Partial data dumps using Postgres Row Level Security - https://supabase.com/blog/partial-postgresql-data-dumps-with-rls - Using RLS to create seed files for local PostgreSQL testing. - Mon, 27 Jun 2022 02:02:30 +0000 - - - https://supabase.com/blog/audit - Postgres Auditing in 150 lines of SQL - https://supabase.com/blog/audit - PostgreSQL has a robust set of features which we can leverage to create a generic auditing solution in 150 lines of SQL. - Mon, 07 Mar 2022 03:03:30 +0000 - - - https://supabase.com/blog/roles-postgres-hooks - Protecting reserved roles with PostgreSQL Hooks - https://supabase.com/blog/roles-postgres-hooks - Using Postgres Hooks to protect functionality in your Postgres database. - Thu, 01 Jul 2021 02:02:30 +0000 - - - https://supabase.com/blog/postgres-as-a-cron-server - Postgres as a CRON Server - https://supabase.com/blog/postgres-as-a-cron-server - Running repetitive tasks with your Postgres database. - Fri, 05 Mar 2021 03:03:30 +0000 - - - https://supabase.com/blog/cracking-postgres-interview - Cracking PostgreSQL Interview Questions - https://supabase.com/blog/cracking-postgres-interview - Understand the top PostgreSQL Interview Questions - Sat, 27 Feb 2021 03:03:30 +0000 - - - https://supabase.com/blog/postgresql-views - Postgres Views - https://supabase.com/blog/postgresql-views - Creating and using a view in PostgreSQL. - Wed, 18 Nov 2020 03:03:30 +0000 - - - https://supabase.com/blog/continuous-postgresql-backup-walg - Continuous PostgreSQL Backups using WAL-G - https://supabase.com/blog/continuous-postgresql-backup-walg - Have you ever wanted to restore your database's state to a particular moment in time? This post explains how, using WAL-G. - Sun, 02 Aug 2020 02:02:30 +0000 - - - https://supabase.com/blog/postgresql-templates - What are PostgreSQL Templates? - https://supabase.com/blog/postgresql-templates - What are PostgreSQL templates and what are they used for? - Thu, 09 Jul 2020 02:02:30 +0000 - - - https://supabase.com/blog/postgresql-physical-logical-backups - Physical vs Logical Backups in PostgreSQL - https://supabase.com/blog/postgresql-physical-logical-backups - What are physical and logical backups in Postgres? - Tue, 07 Jul 2020 02:02:30 +0000 - - - - diff --git a/apps/www/public/planetpg-steve_chavez-rss.xml b/apps/www/public/planetpg-steve_chavez-rss.xml new file mode 100644 index 00000000000..575cafb6b06 --- /dev/null +++ b/apps/www/public/planetpg-steve_chavez-rss.xml @@ -0,0 +1,26 @@ + + + + 'Postgres | Supabase Blog + https://supabase.com/blog + Latest Postgres news from Steve Chavez at Supabase + en + Thu, 15 Dec 2022 03:03:30 +0000 + + + https://supabase.com/blog/postgrest-11-prerelease + PostgREST 11 pre-release + https://supabase.com/blog/postgrest-11-prerelease + Describes new features of PostgREST 11 pre-release + Thu, 15 Dec 2022 03:03:30 +0000 + + + https://supabase.com/blog/roles-postgres-hooks + Protecting reserved roles with PostgreSQL Hooks + https://supabase.com/blog/roles-postgres-hooks + Using Postgres Hooks to protect functionality in your Postgres database. + Thu, 01 Jul 2021 02:02:30 +0000 + + + + diff --git a/apps/www/public/planetpg-victor-rss.xml b/apps/www/public/planetpg-victor-rss.xml new file mode 100644 index 00000000000..e742fb6eec8 --- /dev/null +++ b/apps/www/public/planetpg-victor-rss.xml @@ -0,0 +1,33 @@ + + + + 'Postgres | Supabase Blog + https://supabase.com/blog + Latest Postgres news from Victor at Supabase + en + Thu, 13 Oct 2022 02:02:30 +0000 + + + https://supabase.com/blog/postgres-full-text-search-vs-the-rest + Postgres Full Text Search vs the rest + https://supabase.com/blog/postgres-full-text-search-vs-the-rest + Comparing one of the most popular Postgres features against alternatives + Thu, 13 Oct 2022 02:02:30 +0000 + + + https://supabase.com/blog/choosing-a-postgres-primary-key + Choosing a Postgres Primary Key + https://supabase.com/blog/choosing-a-postgres-primary-key + Turns out the question of which identifier to use as a Primary Key is complicated -- we're going to dive into some of the complexity and inherent trade-offs, and figure things out + Wed, 07 Sep 2022 02:02:30 +0000 + + + https://supabase.com/blog/seen-by-in-postgresql + Implementing "seen by" functionality with Postgres + https://supabase.com/blog/seen-by-in-postgresql + Different approaches for tracking visitor counts with PostgreSQL. + Sun, 17 Jul 2022 02:02:30 +0000 + + + + diff --git a/docker/docker-compose.yml b/docker/docker-compose.yml index 1f70d259a58..db91090408e 100644 --- a/docker/docker-compose.yml +++ b/docker/docker-compose.yml @@ -105,7 +105,7 @@ services: realtime: container_name: realtime-dev.supabase-realtime - image: supabase/realtime:v2.0.2 + image: supabase/realtime:v2.1.0 depends_on: db: # Disable this if you are using an external Postgres database condition: service_healthy diff --git a/package-lock.json b/package-lock.json index 51dde1c8beb..95377a815ed 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9706,75 +9706,6 @@ "tailwindcss": ">=3.0.0 || >= 3.0.0-alpha.1" } }, - "node_modules/@tanstack/match-sorter-utils": { - "version": "8.1.1", - "resolved": "https://registry.npmjs.org/@tanstack/match-sorter-utils/-/match-sorter-utils-8.1.1.tgz", - "integrity": "sha512-IdmEekEYxQsoLOR0XQyw3jD1GujBpRRYaGJYQUw1eOT1eUugWxdc7jomh1VQ1EKHcdwDLpLaCz/8y4KraU4T9A==", - "dependencies": { - "remove-accents": "0.4.2" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/kentcdodds" - } - }, - "node_modules/@tanstack/query-core": { - "version": "4.15.1", - "resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-4.15.1.tgz", - "integrity": "sha512-+UfqJsNbPIVo0a9ANW0ZxtjiMfGLaaoIaL9vZeVycvmBuWywJGtSi7fgPVMCPdZQFOzMsaXaOsDtSKQD5xLRVQ==", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/tannerlinsley" - } - }, - "node_modules/@tanstack/react-query": { - "version": "4.16.1", - "resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-4.16.1.tgz", - "integrity": "sha512-PDE9u49wSDykPazlCoLFevUpceLjQ0Mm8i6038HgtTEKb/aoVnUZdlUP7C392ds3Cd75+EGlHU7qpEX06R7d9Q==", - "dependencies": { - "@tanstack/query-core": "4.15.1", - "use-sync-external-store": "^1.2.0" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/tannerlinsley" - }, - "peerDependencies": { - "react": "^16.8.0 || ^17.0.0 || ^18.0.0", - "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0", - "react-native": "*" - }, - "peerDependenciesMeta": { - "react-dom": { - "optional": true - }, - "react-native": { - "optional": true - } - } - }, - "node_modules/@tanstack/react-query-devtools": { - "version": "4.16.1", - "resolved": "https://registry.npmjs.org/@tanstack/react-query-devtools/-/react-query-devtools-4.16.1.tgz", - "integrity": "sha512-VrDYLmG+OOcvGSZL5avG4R8jhqeMFP7pzW2sh2BWEV9UfI+aocG+CW8y8ygacxuKy48m8Tyo/xfe8H1z9BGb+g==", - "dependencies": { - "@tanstack/match-sorter-utils": "8.1.1", - "superjson": "^1.10.0", - "use-sync-external-store": "^1.2.0" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/tannerlinsley" - }, - "peerDependencies": { - "@tanstack/react-query": "4.16.1", - "react": "^16.8.0 || ^17.0.0 || ^18.0.0", - "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0" - } - }, "node_modules/@testdeck/core": { "version": "0.2.2", "resolved": "https://registry.npmjs.org/@testdeck/core/-/core-0.2.2.tgz", @@ -37592,8 +37523,8 @@ "@supabase/react-data-grid": "^7.1.0-beta.7", "@supabase/shared-types": "^0.1.29", "@supabase/supabase-js": "^2.1.1", - "@tanstack/react-query": "^4.16.1", - "@tanstack/react-query-devtools": "^4.16.1", + "@tanstack/react-query": "^4.22.0", + "@tanstack/react-query-devtools": "^4.22.0", "@zip.js/zip.js": "^2.6.61", "ajv": "^8.6.3", "analytics-node": "^3.5.0", @@ -37727,6 +37658,75 @@ "tailwindcss": ">=3.0.0 || insiders" } }, + "studio/node_modules/@tanstack/match-sorter-utils": { + "version": "8.7.6", + "resolved": "https://registry.npmjs.org/@tanstack/match-sorter-utils/-/match-sorter-utils-8.7.6.tgz", + "integrity": "sha512-2AMpRiA6QivHOUiBpQAVxjiHAA68Ei23ZUMNaRJrN6omWiSFLoYrxGcT6BXtuzp0Jw4h6HZCmGGIM/gbwebO2A==", + "dependencies": { + "remove-accents": "0.4.2" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/kentcdodds" + } + }, + "studio/node_modules/@tanstack/query-core": { + "version": "4.22.0", + "resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-4.22.0.tgz", + "integrity": "sha512-OeLyBKBQoT265f5G9biReijeP8mBxNFwY7ZUu1dKL+YzqpG5q5z7J/N1eT8aWyKuhyDTiUHuKm5l+oIVzbtrjw==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + } + }, + "studio/node_modules/@tanstack/react-query": { + "version": "4.22.0", + "resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-4.22.0.tgz", + "integrity": "sha512-P9o+HjG42uB/xHR6dMsJaPhtZydSe4v0xdG5G/cEj1oHZAXelMlm67/rYJNQGKgBamKElKogj+HYGF+NY2yHYg==", + "dependencies": { + "@tanstack/query-core": "4.22.0", + "use-sync-external-store": "^1.2.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0", + "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0", + "react-native": "*" + }, + "peerDependenciesMeta": { + "react-dom": { + "optional": true + }, + "react-native": { + "optional": true + } + } + }, + "studio/node_modules/@tanstack/react-query-devtools": { + "version": "4.22.0", + "resolved": "https://registry.npmjs.org/@tanstack/react-query-devtools/-/react-query-devtools-4.22.0.tgz", + "integrity": "sha512-YeYFBnfqvb+ZlA0IiJqiHNNSzepNhI1p2o9i8NlhQli9+Zrn230M47OBaBUs8qr3DD1dC2zGB1Dis50Ktz8gAA==", + "dependencies": { + "@tanstack/match-sorter-utils": "^8.7.0", + "superjson": "^1.10.0", + "use-sync-external-store": "^1.2.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + }, + "peerDependencies": { + "@tanstack/react-query": "4.22.0", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0", + "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0" + } + }, "studio/node_modules/@types/d3-path": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/@types/d3-path/-/d3-path-2.0.2.tgz", @@ -44538,38 +44538,6 @@ "mini-svg-data-uri": "^1.2.3" } }, - "@tanstack/match-sorter-utils": { - "version": "8.1.1", - "resolved": "https://registry.npmjs.org/@tanstack/match-sorter-utils/-/match-sorter-utils-8.1.1.tgz", - "integrity": "sha512-IdmEekEYxQsoLOR0XQyw3jD1GujBpRRYaGJYQUw1eOT1eUugWxdc7jomh1VQ1EKHcdwDLpLaCz/8y4KraU4T9A==", - "requires": { - "remove-accents": "0.4.2" - } - }, - "@tanstack/query-core": { - "version": "4.15.1", - "resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-4.15.1.tgz", - "integrity": "sha512-+UfqJsNbPIVo0a9ANW0ZxtjiMfGLaaoIaL9vZeVycvmBuWywJGtSi7fgPVMCPdZQFOzMsaXaOsDtSKQD5xLRVQ==" - }, - "@tanstack/react-query": { - "version": "4.16.1", - "resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-4.16.1.tgz", - "integrity": "sha512-PDE9u49wSDykPazlCoLFevUpceLjQ0Mm8i6038HgtTEKb/aoVnUZdlUP7C392ds3Cd75+EGlHU7qpEX06R7d9Q==", - "requires": { - "@tanstack/query-core": "4.15.1", - "use-sync-external-store": "^1.2.0" - } - }, - "@tanstack/react-query-devtools": { - "version": "4.16.1", - "resolved": "https://registry.npmjs.org/@tanstack/react-query-devtools/-/react-query-devtools-4.16.1.tgz", - "integrity": "sha512-VrDYLmG+OOcvGSZL5avG4R8jhqeMFP7pzW2sh2BWEV9UfI+aocG+CW8y8ygacxuKy48m8Tyo/xfe8H1z9BGb+g==", - "requires": { - "@tanstack/match-sorter-utils": "8.1.1", - "superjson": "^1.10.0", - "use-sync-external-store": "^1.2.0" - } - }, "@testdeck/core": { "version": "0.2.2", "resolved": "https://registry.npmjs.org/@testdeck/core/-/core-0.2.2.tgz", @@ -63380,8 +63348,8 @@ "@supabase/shared-types": "^0.1.29", "@supabase/supabase-js": "^2.1.1", "@tailwindcss/typography": "^0.5.2", - "@tanstack/react-query": "^4.16.1", - "@tanstack/react-query-devtools": "^4.16.1", + "@tanstack/react-query": "^4.22.0", + "@tanstack/react-query-devtools": "^4.22.0", "@testing-library/dom": "^8.19.0", "@testing-library/react": "^12.1.5", "@testing-library/react-hooks": "^8.0.1", @@ -63500,6 +63468,38 @@ "postcss-selector-parser": "6.0.10" } }, + "@tanstack/match-sorter-utils": { + "version": "8.7.6", + "resolved": "https://registry.npmjs.org/@tanstack/match-sorter-utils/-/match-sorter-utils-8.7.6.tgz", + "integrity": "sha512-2AMpRiA6QivHOUiBpQAVxjiHAA68Ei23ZUMNaRJrN6omWiSFLoYrxGcT6BXtuzp0Jw4h6HZCmGGIM/gbwebO2A==", + "requires": { + "remove-accents": "0.4.2" + } + }, + "@tanstack/query-core": { + "version": "4.22.0", + "resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-4.22.0.tgz", + "integrity": "sha512-OeLyBKBQoT265f5G9biReijeP8mBxNFwY7ZUu1dKL+YzqpG5q5z7J/N1eT8aWyKuhyDTiUHuKm5l+oIVzbtrjw==" + }, + "@tanstack/react-query": { + "version": "4.22.0", + "resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-4.22.0.tgz", + "integrity": "sha512-P9o+HjG42uB/xHR6dMsJaPhtZydSe4v0xdG5G/cEj1oHZAXelMlm67/rYJNQGKgBamKElKogj+HYGF+NY2yHYg==", + "requires": { + "@tanstack/query-core": "4.22.0", + "use-sync-external-store": "^1.2.0" + } + }, + "@tanstack/react-query-devtools": { + "version": "4.22.0", + "resolved": "https://registry.npmjs.org/@tanstack/react-query-devtools/-/react-query-devtools-4.22.0.tgz", + "integrity": "sha512-YeYFBnfqvb+ZlA0IiJqiHNNSzepNhI1p2o9i8NlhQli9+Zrn230M47OBaBUs8qr3DD1dC2zGB1Dis50Ktz8gAA==", + "requires": { + "@tanstack/match-sorter-utils": "^8.7.0", + "superjson": "^1.10.0", + "use-sync-external-store": "^1.2.0" + } + }, "@types/d3-path": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/@types/d3-path/-/d3-path-2.0.2.tgz", diff --git a/spec/common-client-libs-sections.json b/spec/common-client-libs-sections.json index 14606af13be..c290fb91859 100644 --- a/spec/common-client-libs-sections.json +++ b/spec/common-client-libs-sections.json @@ -3,15 +3,13 @@ "title": "Introduction", "id": "introduction", "slug": "introduction", - "type": "markdown", - "libs": ["js", "js_v1", "dart", "dart_v0"] + "type": "markdown" }, { "title": "Installing", "id": "installing", "slug": "installing", - "type": "markdown", - "libs": ["js", "js_v1", "dart", "dart_v0"] + "type": "markdown" }, { "title": "Initializing", @@ -23,67 +21,59 @@ "title": "Typescript Support", "id": "typescript-support", "slug": "typescript-support", - "type": "markdown", - "libs": ["js", "js_v1"] + "type": "markdown" }, { "title": "Upgrade guide", "id": "upgrade-guide", "slug": "upgrade-guide", - "type": "markdown", - "libs": ["js"] + "type": "markdown" }, { "title": "Database", - "libs": ["js", "js_v1", "dart", "dart_v0"], + "items": [ { "id": "select", "title": "Fetch data", "slug": "select", "product": "database", - "type": "function", - "libs": ["js", "js_v1", "dart", "dart_v0"] + "type": "function" }, { "id": "insert", "title": "Insert data", "slug": "insert", "product": "database", - "type": "function", - "libs": ["js", "js_v1", "dart", "dart_v0"] + "type": "function" }, { "id": "update", "title": "Update data", "slug": "update", "product": "database", - "type": "function", - "libs": ["js", "js_v1", "dart", "dart_v0"] + "type": "function" }, { "id": "upsert", "title": "Upsert data", "slug": "upsert", "product": "database", - "type": "function", - "libs": ["js", "js_v1", "dart", "dart_v0"] + "type": "function" }, { "id": "delete", "title": "Delete data", "slug": "delete", "product": "database", - "type": "function", - "libs": ["js", "js_v1", "dart", "dart_v0"] + "type": "function" }, { "id": "rpc", "title": "Call a Postgres function", "slug": "rpc", "product": "database", - "type": "function", - "libs": ["js", "js_v1", "dart", "dart_v0"] + "type": "function" }, { "id": "using-filters", @@ -92,7 +82,7 @@ "slug": "using-filters", "product": "database", "type": "function", - "libs": ["js", "dart", "dart_v0"], + "items": [ { "id": "eq", @@ -100,8 +90,7 @@ "slug": "eq", "product": "database", "parent": "filters", - "type": "function", - "libs": ["js", "dart", "dart_v0"] + "type": "function" }, { "id": "neq", @@ -109,8 +98,7 @@ "slug": "neq", "product": "database", "parent": "filters", - "type": "function", - "libs": ["js", "dart", "dart_v0"] + "type": "function" }, { "id": "gt", @@ -118,8 +106,7 @@ "slug": "gt", "product": "database", "parent": "filters", - "type": "function", - "libs": ["js", "dart", "dart_v0"] + "type": "function" }, { "id": "gte", @@ -127,8 +114,7 @@ "slug": "gte", "product": "database", "parent": "filters", - "type": "function", - "libs": ["js", "dart", "dart_v0"] + "type": "function" }, { "id": "lt", @@ -136,8 +122,7 @@ "slug": "lt", "product": "database", "parent": "filters", - "type": "function", - "libs": ["js", "dart", "dart_v0"] + "type": "function" }, { "id": "lte", @@ -145,8 +130,7 @@ "slug": "lte", "product": "database", "parent": "filters", - "type": "function", - "libs": ["js", "dart", "dart_v0"] + "type": "function" }, { "id": "like", @@ -154,8 +138,7 @@ "slug": "like", "product": "database", "parent": "filters", - "type": "function", - "libs": ["js", "dart", "dart_v0"] + "type": "function" }, { "id": "ilike", @@ -163,8 +146,7 @@ "slug": "ilike", "product": "database", "parent": "filters", - "type": "function", - "libs": ["js", "dart", "dart_v0"] + "type": "function" }, { "id": "is", @@ -172,8 +154,7 @@ "slug": "is", "product": "database", "parent": "filters", - "type": "function", - "libs": ["js", "dart", "dart_v0"] + "type": "function" }, { "id": "in", @@ -181,8 +162,7 @@ "slug": "in", "product": "database", "parent": "filters", - "type": "function", - "libs": ["js", "dart", "dart_v0"] + "type": "function" }, { "id": "contains", @@ -190,8 +170,7 @@ "slug": "contains", "product": "database", "parent": "filters", - "type": "function", - "libs": ["js", "dart", "dart_v0"] + "type": "function" }, { "id": "contained-by", @@ -199,8 +178,7 @@ "slug": "containedby", "product": "database", "parent": "filters", - "type": "function", - "libs": ["js", "dart", "dart_v0"] + "type": "function" }, { "id": "range-gt", @@ -208,8 +186,7 @@ "slug": "rangegt", "product": "database", "parent": "filters", - "type": "function", - "libs": ["js", "dart", "dart_v0"] + "type": "function" }, { "id": "range-gte", @@ -217,8 +194,7 @@ "slug": "rangegte", "product": "database", "parent": "filters", - "type": "function", - "libs": ["js", "dart", "dart_v0"] + "type": "function" }, { "id": "range-lt", @@ -226,8 +202,7 @@ "slug": "rangelt", "product": "database", "parent": "filters", - "type": "function", - "libs": ["js", "dart", "dart_v0"] + "type": "function" }, { "id": "range-lte", @@ -235,8 +210,7 @@ "slug": "rangelte", "product": "database", "parent": "filters", - "type": "function", - "libs": ["js", "dart", "dart_v0"] + "type": "function" }, { "id": "range-adjacent", @@ -244,8 +218,7 @@ "slug": "rangeadjacent", "product": "database", "parent": "filters", - "type": "function", - "libs": ["js", "dart", "dart_v0"] + "type": "function" }, { "id": "overlaps", @@ -253,8 +226,7 @@ "slug": "overlaps", "product": "database", "parent": "filters", - "type": "function", - "libs": ["js", "dart", "dart_v0"] + "type": "function" }, { "id": "text-search", @@ -262,8 +234,7 @@ "slug": "textsearch", "product": "database", "parent": "filters", - "type": "function", - "libs": ["js", "dart", "dart_v0"] + "type": "function" }, { "id": "match", @@ -271,8 +242,7 @@ "slug": "match", "product": "database", "parent": "filters", - "type": "function", - "libs": ["js", "dart", "dart_v0"] + "type": "function" }, { "id": "not", @@ -280,8 +250,7 @@ "slug": "not", "product": "database", "parent": "filters", - "type": "function", - "libs": ["js", "dart", "dart_v0"] + "type": "function" }, { "id": "or", @@ -289,8 +258,7 @@ "slug": "or", "product": "database", "parent": "filters", - "type": "function", - "libs": ["js", "dart", "dart_v0"] + "type": "function" }, { "id": "filter", @@ -298,8 +266,7 @@ "slug": "filter", "product": "database", "parent": "filters", - "type": "function", - "libs": ["js", "dart", "dart_v0"] + "type": "function" } ] }, @@ -311,7 +278,7 @@ "product": "database", "parent": "modifiers", "type": "function", - "libs": ["js", "dart", "dart_v0"], + "items": [ { "id": "db-modifiers-select", @@ -319,8 +286,7 @@ "slug": "db-modifiers-select", "product": "database", "parent": "modifiers", - "type": "function", - "libs": ["js"] + "type": "function" }, { "id": "order", @@ -328,8 +294,7 @@ "slug": "order", "product": "database", "parent": "modifiers", - "type": "function", - "libs": ["js", "dart", "dart_v0"] + "type": "function" }, { "id": "limit", @@ -337,8 +302,7 @@ "slug": "limit", "product": "database", "parent": "modifiers", - "type": "function", - "libs": ["js", "dart", "dart_v0"] + "type": "function" }, { "id": "range", @@ -346,8 +310,7 @@ "slug": "range", "product": "database", "parent": "modifiers", - "type": "function", - "libs": ["js", "dart", "dart_v0"] + "type": "function" }, { "id": "abort-signal", @@ -355,8 +318,7 @@ "slug": "db-abortsignal", "product": "database", "parent": "modifiers", - "type": "function", - "libs": ["js"] + "type": "function" }, { "id": "single", @@ -364,8 +326,7 @@ "slug": "single", "product": "database", "parent": "modifiers", - "type": "function", - "libs": ["js", "dart", "dart_v0"] + "type": "function" }, { "id": "maybe-single", @@ -373,8 +334,7 @@ "slug": "maybesingle", "product": "database", "parent": "modifiers", - "type": "function", - "libs": ["js"] + "type": "function" }, { "id": "csv", @@ -382,8 +342,7 @@ "slug": "db-csv", "product": "database", "parent": "modifiers", - "type": "function", - "libs": ["js"] + "type": "function" } ] } @@ -391,13 +350,13 @@ }, { "title": "Auth", - "libs": ["js", "js_v1", "dart", "dart_v0"], + "items": [ { "id": "auth-api", "title": "Overview", "slug": "auth-api", - "libs": ["js"], + "type": "function" }, { @@ -405,230 +364,202 @@ "title": "Create a new user", "slug": "auth-signup", "product": "auth", - "type": "function", - "libs": ["js", "js_v1", "dart", "dart_v0"] + "type": "function" }, { "id": "sign-in", "title": "Sign in a user", "slug": "auth-signin", "product": "auth", - "type": "function", - "libs": ["js_v1", "dart_v0"] + "type": "function" }, { "id": "sign-in-with-provider", "title": "Sign in a user through OAuth", "slug": "auth-signinwithprovider", "product": "auth", - "type": "function", - "libs": ["dart_v0"] + "type": "function" }, { "id": "sign-in-with-password", "title": "Sign in a user", "slug": "auth-signinwithpassword", "product": "auth", - "type": "function", - "libs": ["js"] + "type": "function" }, { "id": "sign-in-with-otp", "title": "Sign in a user through OTP", "slug": "auth-signinwithotp", "product": "auth", - "type": "function", - "libs": ["js"] + "type": "function" }, { "id": "sign-in-with-oauth", "title": "Sign in a user through OAuth", "slug": "auth-signinwithoauth", "product": "auth", - "type": "function", - "libs": ["js", "dart"] + "type": "function" }, { "id": "sign-out", "title": "Sign out a user", "slug": "auth-signout", "product": "auth", - "type": "function", - "libs": ["js", "js_v1", "dart"] + "type": "function" }, { "id": "session", "title": "Retrieve a session", "slug": "auth-session", "product": "auth", - "type": "function", - "libs": ["js_v1"] + "type": "function" }, { "id": "auth-update", "title": "Update a user", "slug": "auth-update", "product": "auth", - "type": "function", - "libs": ["js_v1"] + "type": "function" }, { "id": "set-auth", "title": "Update the access token", "slug": "auth-setauth", "product": "auth", - "type": "function", - "libs": ["js_v1"] + "type": "function" }, { "id": "user", "title": "Retrieve a user", "slug": "auth-user", "product": "auth", - "type": "function", - "libs": ["js_v1"] + "type": "function" }, { "id": "session", "title": "Retrieve a session", "slug": "auth-session", "product": "auth", - "type": "function", - "libs": ["js_v1"] + "type": "function" }, { "id": "verify-otp", "title": "Verify and log in through OTP", "slug": "auth-verifyotp", "product": "auth", - "type": "function", - "libs": ["js", "dart"] + "type": "function" }, { "id": "current-session", "title": "Retrieve a session", "slug": "auth-currentsession", "product": "auth", - "type": "function", - "libs": ["dart"] + "type": "function" }, { "id": "current-user", "title": "Retrieve a user", "slug": "auth-currentuser", "product": "auth", - "type": "function", - "libs": ["dart"] + "type": "function" }, { "id": "get-session", "title": "Retrieve a session", "slug": "auth-getsession", "product": "auth", - "type": "function", - "libs": ["js"] + "type": "function" }, { "id": "refresh-session", "title": "Retrieve a new session", "slug": "auth-refreshsession", "product": "auth", - "type": "function", - "libs": ["js"] + "type": "function" }, { "id": "get-user", "title": "Retrieve a user", "slug": "auth-getuser", "product": "auth", - "type": "function", - "libs": ["js", "js_v1"] + "type": "function" }, { "id": "update-user", "title": "Update a user", "slug": "auth-updateuser", "product": "auth", - "type": "function", - "libs": ["js", "dart"] + "type": "function" }, { "id": "set-session", "title": "Set the session data", "slug": "auth-setsession", "product": "auth", - "type": "function", - "libs": ["js"] + "type": "function" }, { "id": "on-auth-state-change", "title": "Listen to auth events", "slug": "auth-onauthstatechange", "product": "auth", - "type": "function", - "libs": ["js", "js_v1", "dart", "dart_v0"] + "type": "function" }, { "id": "reset-password-for-email", "title": "Send a password reset request", "slug": "auth-resetpasswordforemail", "product": "auth", - "type": "function", - "libs": ["js", "js_v1", "dart", "dart_v0"] + "type": "function" }, { "id": "mfa-enroll", "title": "Enroll a factor", "slug": "auth-mfa-enroll", "product": "auth", - "type": "function", - "libs": ["js"] + "type": "function" }, { "id": "mfa-challenge", "title": "Create a challenge", "slug": "auth-mfa-challenge", "product": "auth", - "type": "function", - "libs": ["js"] + "type": "function" }, { "id": "mfa-verify", "title": "Verify a challenge", "slug": "auth-mfa-verify", "product": "auth", - "type": "function", - "libs": ["js"] + "type": "function" }, { "id": "mfa-challenge-and-verify", "title": "Create and verify a challenge", "slug": "auth-mfa-challengeandverify", "product": "auth", - "type": "function", - "libs": ["js"] + "type": "function" }, { "id": "mfa-unenroll", "title": "Unenroll a factor", "slug": "auth-mfa-unenroll", "product": "auth", - "type": "function", - "libs": ["js"] + "type": "function" }, { "id": "mfa-get-authenticator-assurance-level", "title": "Get Authenticator Assurance Level", "slug": "auth-mfa-getauthenticatorassurancelevel", "product": "auth", - "type": "function", - "libs": ["js"] + "type": "function" }, { "id": "admin-api", "title": "Auth Admin", "slug": "admin-api", - "libs": ["js", "js_v1"], + "type": "function", "items": [ { @@ -636,88 +567,77 @@ "title": "Retrieve a user", "slug": "auth-admin-getuserbyid", "product": "auth-admin", - "type": "function", - "libs": ["js"] + "type": "function" }, { "id": "list-users", "title": "List all users", "slug": "auth-admin-listusers", "product": "auth-admin", - "type": "function", - "libs": ["js", "js_v1"] + "type": "function" }, { "id": "create-user", "title": "Create a user", "slug": "auth-admin-createuser", "product": "auth-admin", - "type": "function", - "libs": ["js", "js_v1"] + "type": "function" }, { "id": "delete-user", "title": "Delete a user", "slug": "auth-admin-deleteuser", "product": "auth-admin", - "type": "function", - "libs": ["js", "js_v1"] + "type": "function" }, { "id": "invite-user-by-email", "title": "Send an email invite link", "slug": "auth-admin-inviteuserbyemail", "product": "auth-admin", - "type": "function", - "libs": ["js", "js_v1"] + "type": "function" }, { "id": "reset-password-for-email", "title": "Send a password reset request", "slug": "auth-admin-resetpasswordforemail", "product": "auth-admin", - "type": "function", - "libs": ["js"] + "type": "function" }, { "id": "generate-link", "title": "Generate an email link", "slug": "auth-admin-generatelink", "product": "auth-admin", - "type": "function", - "libs": ["js"] + "type": "function" }, { "id": "send-mobile-otp", "title": "Send a one-time passcode", "slug": "auth-api-sendmobileotp", "product": "auth-server", - "type": "function", - "libs": ["js_v1"] + "type": "function" }, { "id": "update-user-by-id", "title": "Update a user", "slug": "auth-admin-updateuserbyid", "product": "auth-admin", - "type": "function", - "libs": ["js", "js_v1"] + "type": "function" }, { "id": "mfa-list-factors", "title": "List all factors for a user", "slug": "auth-admin-mfa-listfactors", "product": "auth-admin", - "type": "function", - "libs": ["js"] + "type": "function" }, { "id": "mfa-delete-factor", "title": "Delete a factor for a user", "slug": "auth-admin-mfa-deletefactor", "product": "auth-admin", - "type": "function", - "libs": ["js"] + "type": "function" } ] } @@ -725,61 +645,55 @@ }, { "title": "Functions", - "libs": ["js", "js_v1", "dart", "dart_v0"], + "items": [ { "id": "invoke", "title": "Invoke a function", "slug": "auth-admin-invoke", "product": "functions", - "type": "function", - "libs": ["js", "js_v1", "dart", "dart_v0"] + "type": "function" } ] }, { "title": "Realtime", - "libs": ["js", "js_v1", "dart", "dart_v0"], + "items": [ { "id": "stream", "title": "Listen to database changes", "slug": "stream", "product": "realtime", - "type": "function", - "libs": ["dart", "dart_v0"] + "type": "function" }, { "id": "subscribe", "title": "Subscribe to database changes", "slug": "subscribe", "product": "realtime", - "type": "function", - "libs": ["js", "js_v1", "dart", "dart_v0"] + "type": "function" }, { "id": "remove-subscription", "title": "Remove a subscription", "slug": "removesubscription", "product": "realtime", - "type": "function", - "libs": ["dart_v0", "js_v1"] + "type": "function" }, { "id": "remove-all-subscriptions", "title": "Remove all subscriptions", "slug": "removeallsubscriptions", "product": "realtime", - "type": "function", - "libs": ["js_v1"] + "type": "function" }, { "id": "get-subscriptions", "title": "Retrieve subscriptions", "slug": "getsubscriptions", "product": "realtime", - "type": "function", - "libs": ["js_v1"] + "type": "function" }, { @@ -787,172 +701,152 @@ "title": "Unsubscribe from a channel", "slug": "removechannel", "product": "realtime", - "type": "function", - "libs": ["js", "dart"] + "type": "function" }, { "id": "remove-all-channels", "title": "Unsubscribe from all channels", "slug": "removeallchannels", "product": "realtime", - "type": "function", - "libs": ["js", "dart"] + "type": "function" }, { "id": "get-channels", "title": "Retrieve all channels", "slug": "getchannels", "product": "realtime", - "type": "function", - "libs": ["js", "dart"] + "type": "function" } ] }, { "title": "Storage", - "libs": ["js", "js_v1", "dart", "dart_v0"], + "items": [ { "id": "create-bucket", "title": "Create a bucket", "slug": "storage-createbucket", "product": "storage", - "type": "function", - "libs": ["js", "js_v1", "dart", "dart_v0"] + "type": "function" }, { "id": "get-bucket", "title": "Retrieve a bucket", "slug": "storage-getbucket", "product": "storage", - "type": "function", - "libs": ["js", "js_v1", "dart", "dart_v0"] + "type": "function" }, { "id": "list-buckets", "title": "List all buckets", "slug": "storage-listbuckets", "product": "storage", - "type": "function", - "libs": ["js", "js_v1", "dart", "dart_v0"] + "type": "function" }, { "id": "update-bucket", "title": "Update a bucket", "slug": "storage-updatebucket", "product": "storage", - "type": "function", - "libs": ["js", "js_v1", "dart", "dart_v0"] + "type": "function" }, { "id": "delete-bucket", "title": "Delete a bucket", "slug": "storage-deletebucket", "product": "storage", - "type": "function", - "libs": ["js", "js_v1", "dart", "dart_v0"] + "type": "function" }, { "id": "empty-bucket", "title": "Empty a bucket", "slug": "storage-emptybucket", "product": "storage", - "type": "function", - "libs": ["js", "js_v1", "dart", "dart_v0"] + "type": "function" }, { "id": "from-upload", "title": "Upload a file", "slug": "storage-from-upload", "product": "storage", - "type": "function", - "libs": ["js", "js_v1", "dart", "dart_v0"] + "type": "function" }, { "id": "from-download", "title": "Download a file", "slug": "storage-from-download", "product": "storage", - "type": "function", - "libs": ["js", "js_v1", "dart", "dart_v0"] + "type": "function" }, { "id": "from-list", "title": "List all files in a bucket", "slug": "storage-from-list", "product": "storage", - "type": "function", - "libs": ["js", "js_v1", "dart", "dart_v0"] + "type": "function" }, { "id": "from-update", "title": "Replace an existing file", "slug": "storage-from-update", "product": "storage", - "type": "function", - "libs": ["js", "js_v1", "dart", "dart_v0"] + "type": "function" }, { "id": "from-move", "title": "Move an existing file", "slug": "storage-from-move", "product": "storage", - "type": "function", - "libs": ["js", "js_v1", "dart", "dart_v0"] + "type": "function" }, { "id": "from-copy", "title": "Copy an existing file", "slug": "storage-from-copy", "product": "storage", - "type": "function", - "libs": ["js", "js_v1"] + "type": "function" }, { "id": "from-remove", "title": "Delete files in a bucket", "slug": "storage-from-remove", "product": "storage", - "type": "function", - "libs": ["js", "js_v1", "dart", "dart_v0"] + "type": "function" }, { "id": "from-create-signed-url", "title": "Create a signed URL", "slug": "storage-from-createsignedurl", "product": "storage", - "type": "function", - "libs": ["js", "js_v1", "dart", "dart_v0"] + "type": "function" }, { "id": "from-create-signed-urls", "title": "Create signed URLs", "slug": "storage-from-createsignedurls", "product": "storage", - "type": "function", - "libs": ["js", "js_v1"] + "type": "function" }, { "id": "from-get-public-url", "title": "Retrieve public URL", "slug": "storage-from-getpublicurl", "product": "storage", - "type": "function", - "libs": ["js", "js_v1", "dart", "dart_v0"] + "type": "function" } ] }, { "title": "Misc", - "libs": ["js", "js_v1"], + "items": [ { "title": "Release Notes", "id": "release-notes", "slug": "release-notes", "isFunc": false, - "type": "markdown", - "libs": ["js", "js_v1"] + "type": "markdown" } ] } diff --git a/spec/common-client-libs.yml b/spec/common-client-libs.yml deleted file mode 100644 index 1d2a2b713c6..00000000000 --- a/spec/common-client-libs.yml +++ /dev/null @@ -1,485 +0,0 @@ -openref: 0.1 - -info: - id: reference/common-library-file - title: Supabase Client libs common - description: | - - This file shares info common between the JS and Dart libraries. - -functions: - - id: initializing - title: initializing - slug: initializing - - - id: select - title: 'Fetching data' - slug: select - product: database - - - id: insert - title: 'Insert data' - slug: insert - product: 'database' - - - id: update - title: 'Update data' - slug: update - product: 'database' - - - id: upsert - title: 'Upsert data' - slug: upsert - product: 'database' - - - id: delete - title: 'Delete data' - slug: delete - product: 'database' - - - id: rpc - title: 'Postgres functions' - slug: rpc - product: 'database' - - - id: using-filters - title: 'Using filters' - slug: using-filters - product: 'database' - - - id: eq - title: eq() - slug: eq - product: 'database' - parent: 'Filters' - - - id: neq - title: neq() - slug: neq - product: 'database' - parent: 'Filters' - - - id: gt - title: gt() - slug: gt - product: 'database' - parent: 'Filters' - - - id: gte - title: gte() - slug: gte - product: 'database' - parent: 'Filters' - - - id: lt - title: lt() - slug: lt - product: 'database' - parent: 'Filters' - - - id: lte - title: lte() - slug: lte - product: 'database' - parent: 'Filters' - - - id: like - title: like() - slug: like - product: 'database' - parent: 'Filters' - - - id: ilike - title: ilike() - slug: ilike - product: 'database' - parent: 'Filters' - - - id: is - title: is() - slug: is - product: 'database' - parent: 'Filters' - - - id: in - title: in() - slug: in - product: 'database' - parent: 'Filters' - - - id: contains - title: contains() - slug: contains - product: 'database' - parent: 'Filters' - - - id: contained-by - title: containedBy() - slug: containedby - product: 'database' - parent: 'Filters' - - - id: range-gt - title: rangeGt() - slug: rangegt - product: 'database' - parent: 'Filters' - - - id: range-gte - title: rangeGte() - slug: rangegte - product: 'database' - parent: 'Filters' - - - id: range-lt - title: rangeLt() - slug: rangelt - product: 'database' - parent: 'Filters' - - - id: range-lte - title: rangeLte() - slug: rangelte - product: 'database' - parent: 'Filters' - - - id: range-adjacent - title: rangeAdjacent() - slug: rangeadjacent - product: 'database' - parent: 'Filters' - - - id: overlaps - title: overlaps() - slug: overlaps - product: 'database' - parent: 'Filters' - - - id: text-search - title: textSearch() - slug: textsearch - product: 'database' - parent: 'Filters' - - - id: match - title: match() - slug: match - product: 'database' - parent: 'Filters' - - - id: not - title: not() - slug: not - product: 'database' - parent: 'Filters' - - - id: or - title: or() - slug: or - product: 'database' - parent: 'Filters' - - - id: filter - title: filter() - slug: filter - product: 'database' - parent: 'Filters' - - - id: using-modifiers - title: Using Modifiers - slug: using-modifiers - product: 'database' - parent: 'Modifiers' - - - id: db-modifiers-select - title: select() - slug: db-modifiers-select - product: 'database' - parent: 'Modifiers' - - - id: order - title: order() - slug: order - product: 'database' - parent: 'Modifiers' - - - id: limit - title: limit() - slug: limit - product: 'database' - parent: 'Modifiers' - - - id: range - title: range() - slug: range - product: 'database' - parent: 'Modifiers' - - - id: abort-signal - title: abortSignal() - slug: db-abortsignal - product: 'database' - parent: 'Modifiers' - - - id: single - title: single() - slug: single - product: 'database' - parent: 'Modifiers' - - - id: maybe-single - title: maybeSingle() - slug: maybesingle - product: 'database' - parent: 'Modifiers' - - - id: csv - title: csv() - slug: db-csv - product: 'database' - parent: 'Modifiers' - - - id: sign-up - title: Sign up - slug: auth-signup - product: 'auth' - - - id: sign-in - title: Sign in - slug: auth-signin - product: 'auth' - - - id: set-auth - title: setAuth() - slug: auth-setauth - product: 'auth' - - - id: send-mobile-otp - title: sendMobileOTP() - slug: auth-api-sendmobileotp - product: 'auth' - - - id: sign-in-with-password - title: Sign in with password - slug: auth-signinwithpassword - product: 'auth' - - - id: sign-in-with-otp - title: Sign in with OTP - slug: auth-signinwithotp - product: 'auth' - - - id: sign-in-with-oauth - title: Sign in with 0auth - slug: auth-signinwithoauth - product: 'auth' - - - id: sign-out - title: signOut() - slug: auth-signout - product: 'auth' - - - id: verify-otp - title: verifyOtp() - slug: auth-verifyotp - product: 'auth' - - - id: get-session - title: getSession() - slug: auth-getsession - product: 'auth' - - - id: get-user - title: getUser() - slug: auth-getuser - product: 'auth' - - - id: update-user - title: updateUser() - slug: auth-updateuser - product: 'auth' - - - id: set-session - title: setSession() - slug: auth-setsession - product: 'auth' - - - id: refresh-session - title: refreshSession() - slug: auth-refreshsession - product: 'auth' - - - id: on-auth-state-change - title: onAuthStateChange() - slug: auth-onauthstatechange - product: 'auth' - - - id: admin-api - title: Overview - slug: supabase-auth-admin-api - product: 'auth-admin' - - - id: get-user-by-id - title: getUserById() - slug: auth-admin-getuserbyid - product: 'auth-admin' - - - id: list-users - title: listUsers() - slug: auth-admin-listusers - product: 'auth-admin' - - - id: create-user - title: createUser() - slug: auth-admin-createuser - product: 'auth-admin' - - - id: delete-user - title: deleteUser() - slug: auth-admin-deleteuser - product: 'auth-admin' - - - id: invite-user-by-email - title: inviteUserByEmail() - slug: auth-admin-inviteuserbyemail - product: 'auth-admin' - - - id: reset-password-for-email - title: resetPasswordForEmail() - slug: auth-admin-resetpasswordforemail - product: 'auth-admin' - - - id: generate-link - title: generateLink() - slug: auth-admin-generatelink - product: 'auth-admin' - - - id: update-user-by-id - title: updateUserById() - slug: auth-admin-updateuserbyid - product: 'auth-admin' - - - id: invoke - title: invoke() - slug: auth-admin-invoke - product: 'functions' - - - id: stream - title: stream() - slug: stream - product: 'realtime' - - - id: subscribe - title: on().subscribe() - slug: subscribe - product: 'realtime' - - - id: remove-subscription - title: removeSubscription() - slug: removesubscription - product: 'realtime' - - - id: remove-all-subscriptions - title: removeallsubscriptions() - slug: removeallsubscriptions - product: 'realtime' - - - id: get-subscriptions - title: getSubscriptions() - slug: getsubscriptions - product: 'realtime' - - - id: get-channels - title: getChannels() - slug: getchannels - product: 'realtime' - - - id: remove-channel - title: removeChannel() - slug: removechannel - product: 'realtime' - - - id: remove-all-channels - title: removeAllChannels() - slug: removeallchannels - product: 'realtime' - - - id: list-buckets - title: listBuckets() - slug: storage-listbuckets - product: 'storage' - - - id: get-bucket - title: getBucket() - slug: storage-getbucket - product: 'storage' - - - id: create-bucket - title: createBucket() - slug: storage-createbucket - product: 'storage' - - - id: empty-bucket - title: emptyBucket() - slug: storage-emptybucket - product: 'storage' - - - id: update-bucket - title: updateBucket() - slug: storage-updatebucket - product: 'storage' - - - id: delete-bucket - title: deleteBucket() - slug: storage-deletebucket - product: 'storage' - - - id: from-upload - title: from.upload() - slug: storage-from-upload - product: 'storage' - - - id: from-update - title: from.update() - slug: storage-from-update - product: 'storage' - - - id: from-move - title: from.move() - slug: storage-from-move - product: 'storage' - - - id: from-copy - title: from.copy() - slug: storage-from-copy - product: 'storage' - - - id: from-create-signed-url - title: from.createSignedUrl() - slug: storage-from-createsignedurl - product: 'storage' - - - id: from-create-signed-urls - title: from.createSignedUrls() - slug: storage-from-createsignedurls - product: 'storage' - - - id: from-get-public-url - title: from.getPublicUrl() - slug: storage-from-getpublicurl - product: 'storage' - - - id: from-download - title: from.download() - slug: storage-from-download - product: 'storage' - - - id: from-remove - title: from.remove() - slug: storage-from-remove - product: 'storage' - - - id: from-list - title: from.list() - slug: storage-from-list - product: 'storage' diff --git a/spec/enrichments/tsdoc_v2/combined.json b/spec/enrichments/tsdoc_v2/combined.json index 0467e800000..0c72523e032 100644 --- a/spec/enrichments/tsdoc_v2/combined.json +++ b/spec/enrichments/tsdoc_v2/combined.json @@ -79271,7 +79271,7 @@ "kindString": "Call signature", "flags": {}, "comment": { - "shortText": "Downloads a file." + "shortText": "Downloads a file from a private bucket. For public buckets, make a request to the URL returned from `getPublicUrl` instead." }, "parameters": [ { diff --git a/spec/enrichments/tsdoc_v2/storage.json b/spec/enrichments/tsdoc_v2/storage.json index d94d2aadde8..475ba699fc3 100644 --- a/spec/enrichments/tsdoc_v2/storage.json +++ b/spec/enrichments/tsdoc_v2/storage.json @@ -5251,7 +5251,7 @@ "kindString": "Call signature", "flags": {}, "comment": { - "shortText": "Downloads a file." + "shortText": "Downloads a file from a private bucket. For public buckets, make a request to the URL returned from `getPublicUrl` instead." }, "parameters": [ { diff --git a/spec/enrichments/tsdoc_v2/storage_dereferenced.json b/spec/enrichments/tsdoc_v2/storage_dereferenced.json index 2074da33442..634dd23b469 100644 --- a/spec/enrichments/tsdoc_v2/storage_dereferenced.json +++ b/spec/enrichments/tsdoc_v2/storage_dereferenced.json @@ -5550,7 +5550,7 @@ "kindString": "Call signature", "flags": {}, "comment": { - "shortText": "Downloads a file." + "shortText": "Downloads a file from a private bucket. For public buckets, make a request to the URL returned from `getPublicUrl` instead." }, "parameters": [ { diff --git a/spec/gotrue_v1_config.yaml b/spec/gotrue_v1_config.yaml index fd7e88431b2..31827802154 100644 --- a/spec/gotrue_v1_config.yaml +++ b/spec/gotrue_v1_config.yaml @@ -40,7 +40,7 @@ parameters: tags: ['general'] # {string[]} These tags are useful for grouping parameters required: true description: | - The base URL of your website. Used as an allow-list for redirects and for constructing URLs used in emails. + The base URL of your website. Used as an allow-list for redirects and for constructing URLs used in emails. - id: 'GOTRUE_URI_ALLOW_LIST' # {string} A unique identifier for this param. title: 'auth.additional_redirect_urls' # {string} Any name. tags: ['general'] # {string[]} These tags are useful for grouping parameters diff --git a/studio/components/interfaces/Database/Wrappers/CreateWrapper.tsx b/studio/components/interfaces/Database/Wrappers/CreateWrapper.tsx new file mode 100644 index 00000000000..dbe8fe10c83 --- /dev/null +++ b/studio/components/interfaces/Database/Wrappers/CreateWrapper.tsx @@ -0,0 +1,294 @@ +import Link from 'next/link' +import { isEmpty } from 'lodash' +import { useState } from 'react' +import { useRouter } from 'next/router' +import { observer } from 'mobx-react-lite' +import { Button, Form, Input, IconArrowLeft, IconExternalLink, IconEdit, IconTrash } from 'ui' +import { PermissionAction } from '@supabase/shared-types/out/constants' + +import { checkPermissions, useParams, useStore } from 'hooks' +import { + FormPanel, + FormActions, + FormSection, + FormsContainer, + FormSectionLabel, + FormSectionContent, +} from 'components/ui/Forms' +import { useFDWCreateMutation } from 'data/fdw/fdw-create-mutation' + +import InputField from './InputField' +import { WRAPPERS } from './Wrappers.constants' +import WrapperTableEditor from './WrapperTableEditor' +import { makeValidateRequired } from './Wrappers.utils' +import { useProjectContext } from 'components/layouts/ProjectLayout/ProjectContext' + +const CreateWrapper = () => { + const formId = 'create-wrapper-form' + const router = useRouter() + const { ui } = useStore() + const { ref, type } = useParams() + const { project } = useProjectContext() + const { mutateAsync: createFDW } = useFDWCreateMutation() + + const [newTables, setNewTables] = useState([]) + const [isEditingTable, setIsEditingTable] = useState(false) + const [selectedTableToEdit, setSelectedTableToEdit] = useState() + const [formErrors, setFormErrors] = useState<{ [k: string]: string }>({}) + + const canCreateWrapper = checkPermissions(PermissionAction.TENANT_SQL_ADMIN_WRITE, 'extensions') + + const wrapperMeta = WRAPPERS.find((wrapper) => wrapper.name === type) + const initialValues = + wrapperMeta !== undefined + ? { + wrapper_name: '', + server_name: '', + ...Object.fromEntries( + wrapperMeta.server.options.map((option) => [option.name, option.defaultValue ?? '']) + ), + } + : {} + + if (wrapperMeta === undefined) { + return ( +
+
+

Unsupported wrapper type

+

+ The wrapper type {type} not supported by the dashboard. Head back to create a different + wrapper. +

+
+ + + + + +
+ ) + } + + const onUpdateTable = (values: any) => { + setNewTables((prev) => { + // if the new values have tableIndex, we are editing an existing table + if (values.tableIndex !== undefined) { + const tableIndex = values.tableIndex + const newTables = [...prev] + delete values.tableIndex + newTables[tableIndex] = values + return newTables + } + return [...prev, values] + }) + setIsEditingTable(false) + setSelectedTableToEdit(undefined) + } + + const onSubmit = async (values: any, { setSubmitting }: any) => { + const validate = makeValidateRequired(wrapperMeta.server.options) + const errors: any = validate(values) + + const { wrapper_name } = values + if (wrapper_name.length === 0) errors.name = 'Please provide a name for your wrapper' + if (newTables.length === 0) errors.tables = 'Please add at least one table' + if (!isEmpty(errors)) return setFormErrors(errors) + + setSubmitting(true) + try { + await createFDW({ + projectRef: project?.ref, + connectionString: project?.connectionString, + wrapperMeta, + formState: { ...values, server_name: `${wrapper_name}_server` }, + tables: newTables, + }) + ui.setNotification({ + category: 'success', + message: `Successfully created ${wrapperMeta.label} foreign data wrapper`, + }) + setNewTables([]) + router.push(`/project/${ref}/database/wrappers`) + } catch (error: any) { + ui.setNotification({ + error, + category: 'error', + message: `Failed to create ${wrapperMeta.label} foreign data wrapper: ${error.message}`, + }) + } finally { + setSubmitting(false) + } + } + + return ( + +
+
+ +

Create a {wrapperMeta?.label} Wrapper

+ +
+ +
+ {({ isSubmitting, handleReset, values, initialValues }: any) => { + const hasChanges = JSON.stringify(values) !== JSON.stringify(initialValues) + return ( + + +
+ } + > + Wrapper Configuration}> + + 0 ? ( + <> + Your wrapper's server name will be{' '} + {values.wrapper_name}_server + + ) : ( + '' + ) + } + /> + + + {wrapperMeta.label} Configuration} + > + + {wrapperMeta.server.options.map((option) => ( + + ))} + + + +

Foreign Tables

+

+ You can query your data from these foreign tables after the wrapper is + created +

+ + } + > + + {newTables.length === 0 ? ( +
+ +
+ ) : ( +
+ {newTables.map((table, i) => ( +
+
+

+ {table.schema_name}.{table.table_name} +

+

+ {wrapperMeta.tables[table.index].label}: {table.columns.join(', ')} +

+
+
+
+
+ ))} +
+ )} + {newTables.length > 0 && ( +
+ +
+ )} + {newTables.length === 0 && formErrors.tables && ( +

{formErrors.tables}

+ )} +
+
+ + ) + }} + + + + { + setSelectedTableToEdit(undefined) + setIsEditingTable(false) + }} + onSave={onUpdateTable} + initialData={selectedTableToEdit} + /> +
+ ) +} + +export default observer(CreateWrapper) diff --git a/studio/components/interfaces/Database/Wrappers/EditWrapper.tsx b/studio/components/interfaces/Database/Wrappers/EditWrapper.tsx new file mode 100644 index 00000000000..2286ec04c7f --- /dev/null +++ b/studio/components/interfaces/Database/Wrappers/EditWrapper.tsx @@ -0,0 +1,392 @@ +import Link from 'next/link' +import { isEmpty } from 'lodash' +import { useEffect, useState } from 'react' +import { useRouter } from 'next/router' +import { observer } from 'mobx-react-lite' +import { + Button, + Form, + Input, + IconArrowLeft, + IconExternalLink, + IconEdit, + IconTrash, + IconLoader, +} from 'ui' +import { PermissionAction } from '@supabase/shared-types/out/constants' + +import { VaultSecret } from 'types' +import { checkPermissions, useImmutableValue, useParams, useStore } from 'hooks' +import { useFDWsQuery } from 'data/fdw/fdws-query' +import { useFDWUpdateMutation } from 'data/fdw/fdw-update-mutation' + +import InputField from './InputField' +import { WRAPPERS } from './Wrappers.constants' +import WrapperTableEditor from './WrapperTableEditor' +import { + formatWrapperTables, + makeValidateRequired, + convertKVStringArrayToJson, +} from './Wrappers.utils' +import Loading from 'components/ui/Loading' +import { + FormPanel, + FormActions, + FormSection, + FormSectionLabel, + FormSectionContent, + FormsContainer, +} from 'components/ui/Forms' +import { useProjectContext } from 'components/layouts/ProjectLayout/ProjectContext' + +const EditWrapper = () => { + const formId = 'edit-wrapper-form' + const router = useRouter() + const { ui, vault } = useStore() + const { ref, id } = useParams() + const { project } = useProjectContext() + + const { data, isLoading } = useFDWsQuery({ + projectRef: project?.ref, + connectionString: project?.connectionString, + }) + + const wrappers = data?.result ?? [] + const foundWrapper = wrappers.find((w) => Number(w.id) === Number(id)) + // this call to useImmutableValue should be removed if the redirect after update is also removed + const wrapper = useImmutableValue(foundWrapper) + + const wrapperMeta = WRAPPERS.find((w) => w.handlerName === wrapper?.handler) + + const { mutateAsync: updateFDW, isLoading: isSaving } = useFDWUpdateMutation() + + const [wrapperTables, setWrapperTables] = useState([]) + const [isEditingTable, setIsEditingTable] = useState(false) + const [selectedTableToEdit, setSelectedTableToEdit] = useState() + const [formErrors, setFormErrors] = useState<{ [k: string]: string }>({}) + + const canCreateWrapper = checkPermissions(PermissionAction.TENANT_SQL_ADMIN_WRITE, 'extensions') + + const initialValues = + wrapperMeta !== undefined + ? { + wrapper_name: wrapper?.name, + server_name: wrapper?.server_name, + ...convertKVStringArrayToJson(wrapper?.server_options ?? []), + } + : {} + + useEffect(() => { + if (wrapper?.id) { + setWrapperTables(formatWrapperTables(wrapper?.tables ?? [])) + } + }, [wrapper?.id]) + + if (isLoading) { + return ( +
+ +
+ ) + } + + if (wrapper === undefined || wrapperMeta === undefined) { + if (isSaving) { + return ( +
+
+ +

Updating wrapper

+
+
+ ) + } else { + return ( +
+
+

Unknown wrapper

+

+ The wrapper ID {id} cannot be found in your project. Head back to select another + wrapper. +

+
+ + + + + +
+ ) + } + } + + const onUpdateTable = (values: any) => { + setWrapperTables((prev) => { + // if the new values have tableIndex, we are editing an existing table + if (values.tableIndex !== undefined) { + const tableIndex = values.tableIndex + const wrapperTables = [...prev] + delete values.tableIndex + wrapperTables[tableIndex] = values + return wrapperTables + } + return [...prev, values] + }) + setIsEditingTable(false) + setSelectedTableToEdit(undefined) + } + + const onSubmit = async (values: any, { setSubmitting }: any) => { + const validate = makeValidateRequired(wrapperMeta.server.options) + const errors: any = validate(values) + + const { wrapper_name } = values + if (wrapper_name.length === 0) errors.name = 'Please provide a name for your wrapper' + if (wrapperTables.length === 0) errors.tables = 'Please add at least one table' + if (!isEmpty(errors)) return setFormErrors(errors) + + setSubmitting(true) + try { + await updateFDW({ + projectRef: project?.ref, + connectionString: project?.connectionString, + wrapper, + wrapperMeta, + formState: { ...values, server_name: `${wrapper_name}_server` }, + tables: wrapperTables, + }) + + ui.setNotification({ + category: 'success', + message: `Successfully updated ${wrapperMeta.label} foreign data wrapper`, + }) + setWrapperTables([]) + router.push(`/project/${ref}/database/wrappers`) + } catch (error: any) { + ui.setNotification({ + error, + category: 'error', + message: `Failed to create ${wrapperMeta.label} foreign data wrapper: ${error.message}`, + }) + } finally { + setSubmitting(false) + } + } + + return ( + +
+
+ +

Edit wrapper: {wrapper.name}

+ +
+ +
+ {({ isSubmitting, handleReset, values, initialValues, resetForm }: any) => { + const [loadingSecrets, setLoadingSecrets] = useState(false) + + const initialTables = formatWrapperTables(wrapper?.tables ?? []) + const hasFormChanges = JSON.stringify(values) !== JSON.stringify(initialValues) + const hasTableChanges = JSON.stringify(initialTables) !== JSON.stringify(wrapperTables) + const hasChanges = hasFormChanges || hasTableChanges + + const encryptedOptions = wrapperMeta.server.options.filter((option) => option.encrypted) + + useEffect(() => { + const fetchEncryptedValues = async () => { + setLoadingSecrets(true) + const res = await Promise.all( + encryptedOptions.map(async (option) => { + const [secret] = vault.listSecrets( + (secret: VaultSecret) => secret.name === `${wrapper.name}_${option.name}` + ) + if (secret !== undefined) { + const value = await vault.fetchSecretValue(secret.id) + return { [option.name]: value } + } else { + return { [option.name]: '' } + } + }) + ) + const secretValues = res.reduce((a: any, b: any) => { + const [key] = Object.keys(b) + return { ...a, [key]: b[key] } + }, {}) + + resetForm({ + values: { ...values, ...secretValues }, + initialValues: { ...initialValues, ...secretValues }, + }) + setLoadingSecrets(false) + } + + if (encryptedOptions.length > 0) fetchEncryptedValues() + }, []) + + return ( + + { + handleReset() + setWrapperTables(initialTables) + }} + helper={ + !canCreateWrapper + ? 'You need additional permissions to create a foreign data wrapper' + : undefined + } + /> +
+ } + > + Wrapper Configuration}> + + + Your wrapper's server name will be updated to{' '} + {values.wrapper_name}_server + + ) : ( + <> + Your wrapper's server name is{' '} + {values.wrapper_name}_server + + ) + } + /> + + + {wrapperMeta.label} Configuration} + > + + {wrapperMeta.server.options.map((option) => ( + + ))} + + + +

Foreign Tables

+

+ You can query your data from these foreign tables after the wrapper is + created +

+ + } + > + + {wrapperTables.length === 0 ? ( +
+ +
+ ) : ( +
+ {wrapperTables.map((table, i) => ( +
+
+

+ {table.schema_name}.{table.table_name} +

+

+ {wrapperMeta.tables[table.index].label}: {table.columns.join(', ')} +

+
+
+
+
+ ))} +
+ )} + {wrapperTables.length > 0 && ( +
+ +
+ )} + {wrapperTables.length === 0 && formErrors.tables && ( +

{formErrors.tables}

+ )} +
+
+ + ) + }} + + + + { + setSelectedTableToEdit(undefined) + setIsEditingTable(false) + }} + onSave={onUpdateTable} + initialData={selectedTableToEdit} + /> +
+ ) +} + +export default observer(EditWrapper) diff --git a/studio/components/interfaces/Database/Wrappers/InputField.tsx b/studio/components/interfaces/Database/Wrappers/InputField.tsx new file mode 100644 index 00000000000..a3839cc8fad --- /dev/null +++ b/studio/components/interfaces/Database/Wrappers/InputField.tsx @@ -0,0 +1,94 @@ +import Link from 'next/link' +import { FC, useState } from 'react' +import { Button, Input, IconHelpCircle, IconEye, IconEyeOff, IconLoader } from 'ui' +import { ServerOption } from './Wrappers.types' + +interface Props { + option: ServerOption + loading: boolean + error: any +} + +const InputField: FC = ({ option, loading, error }) => { + const [showHidden, setShowHidden] = useState(!option.hidden) + if (option.isTextArea) { + return ( +
+ +

{option.label}

+ {option.urlHelper !== undefined && ( + + + + + + )} +
+ } + value={loading ? 'Fetching value from Vault...' : undefined} + defaultValue={option.defaultValue ?? ''} + required={option.required ?? false} + /> + + ) + } else { + return ( + +

{option.label}

+ {option.urlHelper !== undefined && ( + + + + + + )} + + } + defaultValue={option.defaultValue ?? ''} + error={error} + value={loading ? 'Fetching value from Vault...' : undefined} + type={!option.hidden || loading ? 'text' : showHidden ? 'text' : 'password'} + disabled={loading} + actions={ + loading ? ( +
+
+ ) : option.hidden ? ( +
+
+ ) : null + } + /> + ) + } +} + +export default InputField diff --git a/studio/components/interfaces/Database/Wrappers/WrapperRow.tsx b/studio/components/interfaces/Database/Wrappers/WrapperRow.tsx index 75368404eef..35b7efbed34 100644 --- a/studio/components/interfaces/Database/Wrappers/WrapperRow.tsx +++ b/studio/components/interfaces/Database/Wrappers/WrapperRow.tsx @@ -1,222 +1,50 @@ import Link from 'next/link' import Image from 'next/image' -import { isEmpty } from 'lodash' -import { FC, useState } from 'react' -import { - Collapsible, - Input, - IconCheck, - IconChevronUp, - Button, - IconEdit, - IconTrash, - IconEye, - IconEyeOff, - IconCheckCircle, - IconExternalLink, - IconLoader, - IconHelpCircle, -} from 'ui' +import { FC } from 'react' +import { Collapsible, IconChevronUp, Button, IconExternalLink, IconTrash, IconEdit } from 'ui' +import { partition } from 'lodash' import { useParams, useStore } from 'hooks' -import { ServerOption, Wrapper } from './Wrappers.types' -import { makeValidateRequired } from './Wrappers.utils' -import { useFDWCreateMutation } from 'data/fdw/fdw-create-mutation' +import { WrapperMeta } from './Wrappers.types' +import { FDW } from 'data/fdw/fdws-query' import { useFDWDeleteMutation } from 'data/fdw/fdw-delete-mutation' import { useProjectContext } from 'components/layouts/ProjectLayout/ProjectContext' -import WrapperTableEditor from './WrapperTableEditor' -import InformationBox from 'components/ui/InformationBox' import { confirmAlert } from 'components/to-be-cleaned/ModalsDeprecated/ConfirmModal' interface Props { - wrapper: Wrapper - tables: any[] - isLoading: boolean - isEnabled: boolean + wrappers: FDW[] + wrapperMeta: WrapperMeta isOpen: boolean onOpen: (wrapper: string) => void } -const InputField: FC<{ - option: ServerOption - value: any - error: any - onChange: (e: any) => void -}> = ({ option, value, error, onChange }) => { - const [showHidden, setShowHidden] = useState(!option.hidden) - - if (option.isTextArea) { - return ( -
- -

{option.label}

- {option.urlHelper !== undefined && ( - - - - - - )} -
- } - defaultValue={option.defaultValue ?? ''} - required={option.required ?? false} - value={value} - onChange={onChange} - error={error} - className="input-mono" - rows={6} - /> - - ) - } else { - return ( - -

{option.label}

- {option.urlHelper !== undefined && ( - - - - - - )} - - } - defaultValue={option.defaultValue ?? ''} - required={option.required ?? false} - value={value} - onChange={onChange} - error={error} - className="input-mono" - type={!option.hidden ? 'text' : showHidden ? 'text' : 'password'} - actions={ - option.hidden ? ( -
-
- ) : null - } - /> - ) - } -} - -const WrapperRow: FC = ({ wrapper, tables, isLoading, isEnabled, isOpen, onOpen }) => { - const getInitialFormState = () => - Object.fromEntries( - wrapper.server.options.map((option) => [option.name, option.defaultValue ?? '']) - ) - +const WrapperRow: FC = ({ wrappers = [], wrapperMeta, isOpen, onOpen }) => { const { ui } = useStore() const { ref } = useParams() const { project } = useProjectContext() - const { mutateAsync: createFDW } = useFDWCreateMutation() const { mutateAsync: deleteFDW } = useFDWDeleteMutation() - const [isSubmitting, setIsSubmitting] = useState(false) - const [isEditingTable, setIsEditingTable] = useState(false) - const [selectedTableToEdit, setSelectedTableToEdit] = useState() - - const [newTables, setNewTables] = useState([]) - const [formState, setFormState] = useState(getInitialFormState) - const [formErrors, setFormErrors] = useState<{ [k: string]: string }>({}) - - const noChanges = JSON.stringify(getInitialFormState()) === JSON.stringify(formState) - - const onUpdateTable = (values: any) => { - setNewTables((prev) => { - // if the new values have tableIndex, we are editing an existing table - if (values.tableIndex !== undefined) { - const tableIndex = values.tableIndex - const newTables = [...prev] - delete values.tableIndex - newTables[tableIndex] = values - return newTables - } - return [...prev, values] - }) - setIsEditingTable(false) - setSelectedTableToEdit(undefined) - } - - const onSaveWrapper = async () => { - const validate = makeValidateRequired(wrapper.server.options) - const errors: any = validate(formState) - if (newTables.length === 0) errors.tables = 'Please add at least one table' - - if (!isEmpty(errors)) { - setFormErrors(errors) - return - } - - setIsSubmitting(true) - try { - await createFDW({ - projectRef: project?.ref, - connectionString: project?.connectionString, - wrapper, - formState, - newTables, - }) - ui.setNotification({ - category: 'success', - message: `Successfully created ${wrapper.label} foreign data wrapper`, - }) - setNewTables([]) - setFormState(getInitialFormState) - } catch (error: any) { - ui.setNotification({ - error, - category: 'error', - message: `Failed to create ${wrapper.label} foreign data wrapper: ${error.message}`, - }) - } finally { - setIsSubmitting(false) - } - } - - const onDeleteWrapper = () => { + const onDeleteWrapper = (wrapper: any) => { confirmAlert({ - title: `Confirm to disable ${wrapper.label} wrapper`, - message: `Are you sure you want to disable the ${wrapper.label} wrapper? This will also remove all tables created with this wrapper.`, + title: `Confirm to disable ${wrapper.name}`, + message: `Are you sure you want to disable the ${wrapper.name} wrapper? This will also remove all tables created with this wrapper.`, onAsyncConfirm: async () => { try { await deleteFDW({ projectRef: project?.ref, connectionString: project?.connectionString, wrapper, + wrapperMeta, }) ui.setNotification({ category: 'success', - message: `Successfully removed ${wrapper.label} foreign data wrapper`, + message: `Successfully disabled ${wrapper.name} foreign data wrapper`, }) } catch (error: any) { ui.setNotification({ error, category: 'error', - message: `Disabling ${wrapper.name} failed: ${error.message}`, + message: `Failed to disable ${wrapper.name}: ${error.message}`, }) } }, @@ -227,7 +55,7 @@ const WrapperRow: FC = ({ wrapper, tables, isLoading, isEnabled, isOpen, <> onOpen(wrapper.name)} + onOpenChange={() => onOpen(wrapperMeta.name)} className={[ 'bg-scale-100 dark:bg-scale-300 ', 'hover:bg-scale-200 dark:hover:bg-scale-500', @@ -235,11 +63,11 @@ const WrapperRow: FC = ({ wrapper, tables, isLoading, isEnabled, isOpen, 'border-scale-300', 'dark:border-scale-500 hover:border-scale-500', 'dark:hover:border-scale-700 data-open:border-scale-700', - 'data-open:pb-px col-span-12 mx-auto', + 'col-span-12 mx-auto', '-space-y-px overflow-hidden', 'transition border shadow hover:z-50', - 'first:rounded-tl first:rounded-tr', - 'last:rounded-bl last:rounded-br', + 'first:rounded-tl first:rounded-tr first:!border-t', + 'last:rounded-bl last:rounded-br last:border-t-0', ].join(' ')} > @@ -254,209 +82,102 @@ const WrapperRow: FC = ({ wrapper, tables, isLoading, isEnabled, isOpen, width={14} /> {`${wrapper.name} - {wrapper.label} + {wrapperMeta.label}
- {isLoading ? ( -
- -
- ) : isEnabled ? ( -
- - - - Enabled -
- ) : ( -
- Disabled -
- )} +
+ {wrappers.length} wrapper{wrappers.length > 1 ? 's' : ''} +
- {!isLoading && ( - <> - {isEnabled ? ( - -
-
- - } - title={`${wrapper.label} foreign data wrapper is currently enabled`} - description="If you'd like to edit this wrapper, you'll need to disable the wrapper first and create it again with any updated configuration." - /> -
-

Foreign Tables

-
- {tables.map((table) => ( -
-
-

- {table.schema}.{table.name} -

-

- Columns: {table.columns.map((c: any) => c.name).join(', ')} -

-
- - - - - -
+ +
+ {wrappers.map((wrapper) => { + const serverOptions = Object.fromEntries( + wrapper.server_options.map((option: any) => option.split('=')) + ) + const [encryptedMetadata, visibleMetadata] = partition( + wrapperMeta.server.options, + 'hidden' + ) + + return ( +
+
+
+

{wrapper.name}

+
+ {visibleMetadata.map((metadata) => ( +
+

{metadata.label}:

+

{serverOptions[metadata.name]}

+
+ ))} + {encryptedMetadata.map((metadata) => ( +
+

{metadata.label}:

+ + + Encrypted in Vault + + + +
+ ))} +
+

+ Foreign tables: ({wrapper.tables.length}) +

+
+ {wrapper.tables.map((table: any) => ( + + +
+ {table.name} +
+
+ ))}
- + icon={} + className="py-2" + /> + + +
- - ) : ( - -
-
- {wrapper.server.options.map((option) => ( - { - setFormState((prev) => ({ ...prev, [option.name]: e.target.value })) - setFormErrors((prev) => ({ ...prev, [option.name]: '' })) - }} - /> - ))} -
-
-
-

Foreign Tables

- -
-
-
- {newTables.length === 0 && ( -
-

- Add foreign tables to query from after the wrapper is enabled -

-
- )} - {newTables.map((table, i) => ( -
-
-

- {table.schema_name}.{table.table_name} -

-

- {wrapper.tables[table.index].label}: {table.columns.join(', ')} -

-
-
-
-
- ))} - {newTables.length === 0 && formErrors.tables && ( -

{formErrors.tables}

- )} -
-
- - - - - -
- - -
-
-
-
-
-
- )} - - )} + ) + })} +
+
- { - setSelectedTableToEdit(undefined) - setIsEditingTable(false) - }} - onSave={onUpdateTable} - initialData={selectedTableToEdit} - /> ) } diff --git a/studio/components/interfaces/Database/Wrappers/Wrappers.constants.ts b/studio/components/interfaces/Database/Wrappers/Wrappers.constants.ts index a987a4533af..cbb51c09408 100644 --- a/studio/components/interfaces/Database/Wrappers/Wrappers.constants.ts +++ b/studio/components/interfaces/Database/Wrappers/Wrappers.constants.ts @@ -1,6 +1,6 @@ -import { Wrapper } from './Wrappers.types' +import { WrapperMeta } from './Wrappers.types' -export const WRAPPERS: Wrapper[] = [ +export const WRAPPERS: WrapperMeta[] = [ { name: 'stripe_wrapper', handlerName: 'stripe_fdw_handler', @@ -10,7 +10,6 @@ export const WRAPPERS: Wrapper[] = [ label: 'Stripe', docsUrl: 'https://supabase.com/docs/guides/database/wrappers/stripe', server: { - name: 'stripe_server', options: [ { name: 'api_key_id', @@ -415,7 +414,6 @@ export const WRAPPERS: Wrapper[] = [ label: 'Firebase', docsUrl: 'https://supabase.com/docs/guides/database/wrappers/firebase', server: { - name: 'firebase_server', options: [ { name: 'project_id', diff --git a/studio/components/interfaces/Database/Wrappers/Wrappers.tsx b/studio/components/interfaces/Database/Wrappers/Wrappers.tsx index bc6abe43bab..d05146b786d 100644 --- a/studio/components/interfaces/Database/Wrappers/Wrappers.tsx +++ b/studio/components/interfaces/Database/Wrappers/Wrappers.tsx @@ -2,34 +2,32 @@ import Link from 'next/link' import { useState } from 'react' import { Button, IconExternalLink } from 'ui' import { observer } from 'mobx-react-lite' -import * as Tooltip from '@radix-ui/react-tooltip' -import { PermissionAction } from '@supabase/shared-types/out/constants' -import { checkPermissions, useParams, useStore } from 'hooks' +import { useStore } from 'hooks' import { useFDWsQuery } from 'data/fdw/fdws-query' import { WRAPPERS } from './Wrappers.constants' import WrapperRow from './WrapperRow' import ShimmeringLoader from 'components/ui/ShimmeringLoader' +import WrappersDropdown from './WrappersDropdown' +import WrappersDisabledState from './WrappersDisabledState' import { useProjectContext } from 'components/layouts/ProjectLayout/ProjectContext' +import { groupBy } from 'lodash' const Wrappers = () => { - const { ref } = useParams() - const { ui, meta } = useStore() + const { meta } = useStore() const { project } = useProjectContext() const { data, isLoading } = useFDWsQuery({ projectRef: project?.ref, connectionString: project?.connectionString, }) - const wrappers = data?.result ?? [] - const enabledWrapperNamesSet = new Set(data?.result.map((fdw) => fdw.name)) - const [open, setOpen] = useState('') - const [isEnabling, setIsEnabling] = useState(false) + + const wrappers = data?.result ?? [] + const groupedWrappers = groupBy(wrappers, 'handler') const wrappersExtension = meta.extensions.byId('wrappers') const vaultExtension = meta.extensions.byId('supabase_vault') const isLoadingExtensions = meta.extensions.isLoading - const isNotAvailable = wrappersExtension === undefined || vaultExtension === undefined const isWrappersEnabled = wrappersExtension !== undefined && @@ -37,48 +35,6 @@ const Wrappers = () => { vaultExtension !== undefined && vaultExtension?.installed_version !== null - const canToggleWrappers = checkPermissions(PermissionAction.TENANT_SQL_ADMIN_WRITE, 'extensions') - - const onEnableWrappers = async () => { - if (wrappersExtension === undefined || vaultExtension === undefined) return - setIsEnabling(true) - - const requiredExtensions = await Promise.all([ - await meta.extensions.create({ - schema: wrappersExtension.schema ?? 'extensions', - name: wrappersExtension.name, - version: wrappersExtension.default_version, - cascade: true, - }), - await meta.extensions.create({ - schema: vaultExtension.schema ?? 'vault', - name: vaultExtension.name, - version: vaultExtension.default_version, - cascade: true, - }), - ]) - const errors = requiredExtensions.filter( - (res) => res.error && !res.error.message.includes('already exists') - ) - - if (errors.length > 0) { - ui.setNotification({ - error: errors, - category: 'error', - message: `Failed to enable Wrappers for your project: ${errors - .map((x) => x.message) - .join(', ')}`, - }) - } else { - ui.setNotification({ - category: 'success', - message: 'Wrappers is now enabled for your project!', - }) - } - - setIsEnabling(false) - } - return (
@@ -88,159 +44,63 @@ const Wrappers = () => { Query your data warehouse directly from your database, or third-party APIs using SQL.
- {/* [Joshen TODO] For when we support multiple instances per wrapper */} - {/*
- - {WRAPPERS.map((wrapper, idx) => ( - <> - - } - onClick={() => {}} - > - {wrapper.label} - - {idx !== WRAPPERS.length - 1 && } - - ))} - - } - > - - -
*/} +
+ + + + + + {isWrappersEnabled && } +
- {isLoadingExtensions ? ( -
+ {isLoadingExtensions || isLoading ? ( +
) : isWrappersEnabled ? (
- {WRAPPERS.map((wrapper, i) => { - return ( - w.name === wrapper.server.name)?.tables ?? []} - isLoading={isLoading} - isEnabled={enabledWrapperNamesSet.has(wrapper.server.name)} - isOpen={open === wrapper.name} - onOpen={(wrapperName) => { - if (open !== wrapperName) setOpen(wrapperName) - else setOpen('') - }} - /> - ) - })} + {wrappers.length === 0 ? ( +
+

No wrappers created yet

+ +
+ ) : ( + <> + {/* [Joshen] This probably needs to change anyways so dont get too stuck with this */} + {WRAPPERS.map((wrapper, i) => { + const createdWrappers = groupedWrappers[wrapper.handlerName] ?? [] + if (createdWrappers.length > 0) { + return ( + { + if (open !== wrapperName) setOpen(wrapperName) + else setOpen('') + }} + /> + ) + } else { + return null + } + })} + + )}
) : ( -
-
-
-
-

Supabase Wrappers

-

- Supabase Wrappers is a framework for building Postgres Foreign Data Wrappers (FDW) - which connect Postgres to external systems. Query your data warehouse or - third-party APIs directly from your database. -

-
- {isNotAvailable ? ( -
-
-
-

- Wrappers is not available for this project yet. -

-

- Do reach out to us if you're interested! -

-
- -
- -
- ) : ( -
- - - - - - - - - - {!canToggleWrappers && ( - - -
- - You need additional permissions to enable Wrappers for this project - -
-
- )} -
-
- )} -
-
-
+ )}
) diff --git a/studio/components/interfaces/Database/Wrappers/Wrappers.types.ts b/studio/components/interfaces/Database/Wrappers/Wrappers.types.ts index 25ac12d203b..c0da532e9f1 100644 --- a/studio/components/interfaces/Database/Wrappers/Wrappers.types.ts +++ b/studio/components/interfaces/Database/Wrappers/Wrappers.types.ts @@ -1,4 +1,4 @@ -export type Wrapper = { +export type WrapperMeta = { name: string handlerName: string validatorName: string @@ -22,7 +22,6 @@ export type ServerOption = { } export type Server = { - name: string options: ServerOption[] } diff --git a/studio/components/interfaces/Database/Wrappers/Wrappers.utils.ts b/studio/components/interfaces/Database/Wrappers/Wrappers.utils.ts index 19b48c207f1..97545babb4f 100644 --- a/studio/components/interfaces/Database/Wrappers/Wrappers.utils.ts +++ b/studio/components/interfaces/Database/Wrappers/Wrappers.utils.ts @@ -19,3 +19,24 @@ export const makeValidateRequired = return errors } + +export const formatWrapperTables = (tables: any[]) => { + return tables.map((table, index: number) => { + const object = table.options.find((option: string) => option.startsWith('object=')) + const objectValue = object !== undefined ? object.split('=')[1] : undefined + + return { + index, + columns: table.columns.map((column: any) => column.name), + is_new_schema: false, + object: objectValue, + schema: table.schema, + schema_name: table.schema, + table_name: table.name, + } + }) +} + +export const convertKVStringArrayToJson = (values: string[]) => { + return Object.fromEntries(values.map((value) => value.split('='))) +} diff --git a/studio/components/interfaces/Database/Wrappers/WrappersDisabledState.tsx b/studio/components/interfaces/Database/Wrappers/WrappersDisabledState.tsx new file mode 100644 index 00000000000..54e4fa6a8b1 --- /dev/null +++ b/studio/components/interfaces/Database/Wrappers/WrappersDisabledState.tsx @@ -0,0 +1,156 @@ +import Link from 'next/link' +import { useState } from 'react' +import { Button, IconExternalLink } from 'ui' +import { observer } from 'mobx-react-lite' +import * as Tooltip from '@radix-ui/react-tooltip' +import { PermissionAction } from '@supabase/shared-types/out/constants' + +import { checkPermissions, useParams, useStore } from 'hooks' + +const WrappersDisabledState = () => { + const { ui, meta } = useStore() + const { ref } = useParams() + const wrappersExtension = meta.extensions.byId('wrappers') + const vaultExtension = meta.extensions.byId('supabase_vault') + const isNotAvailable = wrappersExtension === undefined || vaultExtension === undefined + + const [isEnabling, setIsEnabling] = useState(false) + const canToggleWrappers = checkPermissions(PermissionAction.TENANT_SQL_ADMIN_WRITE, 'extensions') + + const onEnableWrappers = async () => { + if (wrappersExtension === undefined || vaultExtension === undefined) return + setIsEnabling(true) + + const requiredExtensions = await Promise.all([ + await meta.extensions.create({ + schema: wrappersExtension.schema ?? 'extensions', + name: wrappersExtension.name, + version: wrappersExtension.default_version, + cascade: true, + }), + await meta.extensions.create({ + schema: vaultExtension.schema ?? 'vault', + name: vaultExtension.name, + version: vaultExtension.default_version, + cascade: true, + }), + ]) + const errors = requiredExtensions.filter( + (res) => res.error && !res.error.message.includes('already exists') + ) + + if (errors.length > 0) { + ui.setNotification({ + error: errors, + category: 'error', + message: `Failed to enable Wrappers for your project: ${errors + .map((x) => x.message) + .join(', ')}`, + }) + } else { + ui.setNotification({ + category: 'success', + message: 'Wrappers is now enabled for your project!', + }) + } + + setIsEnabling(false) + } + + return ( +
+
+
+
+

Supabase Wrappers

+

+ Supabase Wrappers is a framework for building Postgres Foreign Data Wrappers (FDW) + which connect Postgres to external systems. Query your data warehouse or third-party + APIs directly from your database. +

+
+ {isNotAvailable ? ( +
+
+
+

+ Wrappers is not available for this project yet. +

+

+ Do reach out to us if you're interested! +

+
+ +
+ +
+ ) : ( +
+ + + + + + + + + + {!canToggleWrappers && ( + + +
+ + You need additional permissions to enable Wrappers for this project + +
+
+ )} +
+
+ )} +
+
+
+ ) +} + +export default observer(WrappersDisabledState) diff --git a/studio/components/interfaces/Database/Wrappers/WrappersDropdown.tsx b/studio/components/interfaces/Database/Wrappers/WrappersDropdown.tsx new file mode 100644 index 00000000000..b74c9a9fffb --- /dev/null +++ b/studio/components/interfaces/Database/Wrappers/WrappersDropdown.tsx @@ -0,0 +1,59 @@ +import Link from 'next/link' +import Image from 'next/image' +import { FC, Fragment } from 'react' +import { observer } from 'mobx-react-lite' +import { Button, Dropdown, IconPlus } from 'ui' + +import { useParams } from 'hooks' +import { WRAPPERS } from './Wrappers.constants' + +interface Props { + buttonText?: string + align?: 'center' | 'end' +} + +const WrapperDropdown: FC = ({ buttonText = 'Add wrapper', align = 'end' }) => { + const { ref } = useParams() + + return ( + + {WRAPPERS.map((wrapper, idx) => ( + + + + + } + > + {wrapper.label} + + + + {idx !== WRAPPERS.length - 1 && } + + ))} + + } + > + + + ) +} + +export default observer(WrapperDropdown) diff --git a/studio/components/interfaces/Database/index.ts b/studio/components/interfaces/Database/index.ts index b294434c18e..d866b7d8c8c 100644 --- a/studio/components/interfaces/Database/index.ts +++ b/studio/components/interfaces/Database/index.ts @@ -7,6 +7,8 @@ import RolesSettings from './Roles/RolesSettings' import Extensions from './Extensions/Extensions' import Wrappers from './Wrappers/Wrappers' +import CreateWrapper from './Wrappers/CreateWrapper' +import EditWrapper from './Wrappers/EditWrapper' import PublicationsList from './Publications/PublicationsList' import PublicationsTables from './Publications/PublicationsTables' @@ -28,6 +30,8 @@ export { RolesSettings, Extensions, Wrappers, + CreateWrapper, + EditWrapper, PublicationsList, PublicationsTables, BackupsList, diff --git a/studio/components/interfaces/Settings/Vault/Secrets/SecretsManagement.tsx b/studio/components/interfaces/Settings/Vault/Secrets/SecretsManagement.tsx index 38d514c6c11..468773ef358 100644 --- a/studio/components/interfaces/Settings/Vault/Secrets/SecretsManagement.tsx +++ b/studio/components/interfaces/Settings/Vault/Secrets/SecretsManagement.tsx @@ -1,25 +1,31 @@ import Link from 'next/link' import { observer } from 'mobx-react-lite' -import { FC, Fragment, useState } from 'react' -import { IconSearch, Input, Button, Listbox, IconLoader, IconExternalLink } from 'ui' +import { FC, Fragment, useState, useEffect } from 'react' +import { IconSearch, Input, Button, Listbox, IconLoader, IconExternalLink, IconX } from 'ui' +import { useStore, useParams } from 'hooks' import SecretRow from './SecretRow' import EditSecretModal from './EditSecretModal' import DeleteSecretModal from './DeleteSecretModal' import AddNewSecretModal from './AddNewSecretModal' import Divider from 'components/ui/Divider' -import { useStore } from 'hooks' interface Props {} const SecretsManagement: FC = ({}) => { const { vault } = useStore() + const { search } = useParams() + const [searchValue, setSearchValue] = useState('') const [selectedSort, setSelectedSort] = useState<'updated_at' | 'name'>('updated_at') const [showAddSecretModal, setShowAddSecretModal] = useState(false) const [selectedSecretToEdit, setSelectedSecretToEdit] = useState() const [selectedSecretToRemove, setSelectedSecretToRemove] = useState() + useEffect(() => { + if (search !== undefined) setSearchValue(search) + }, [search]) + const secrets = ( searchValue.length > 0 ? vault.listSecrets( @@ -48,6 +54,19 @@ const SecretsManagement: FC = ({}) => { value={searchValue} onChange={(event) => setSearchValue(event.target.value)} icon={} + actions={ + searchValue.length > 0 + ? [ +
-
- {/* List of tables belonging to selected schema */} - {filteredTables.length > 0 && ( - - -
-
-

Tables

-

- ({filteredTables.length}) -

+ {isFetchingTables ? ( +
+ +

Loading tables...

+
+ ) : searchText.length === 0 && filteredTables.length === 0 && filteredViews.length === 0 ? ( +
+

No tables available

+

This schema has no tables available yet

+
+ ) : ( +
+ {/* List of tables belonging to selected schema */} + {filteredTables.length > 0 && ( + + +
+
+

Tables

+

+ ({filteredTables.length}) +

+
+
- -
- - } - /> + + } + /> -
- {filteredTables.map((table) => { - const isActive = Number(id) === table.id +
+ {filteredTables.map((table) => { + const isActive = Number(id) === table.id + return ( + } + onClick={() => onEditTable(table)} + > + Edit Table + , + } + onClick={() => onDuplicateTable(table)} + > + Duplicate Table + , + + + }> + View Policies + + + , + , + } + onClick={() => onDeleteTable(table)} + > + Delete Table + , + ]} + > +
+ +
+ + ) + } + /> + ) + })} +
+
+ )} + + {/* List of views belonging to selected schema */} + {filteredViews.length > 0 && ( + + +

Views

+

({filteredViews.length})

+
+ } + /> + + {filteredViews.map((view: SchemaView) => { + const isActive = Number(id) === view.id return ( - } - onClick={() => onEditTable(table)} - > - Edit Table - , - } - onClick={() => onDuplicateTable(table)} - > - Duplicate Table - , - - - }> - View Policies - - - , - , - } - onClick={() => onDeleteTable(table)} - > - Delete Table - , - ]} - > -
- -
- - ) - } - /> + + + +
+

{view.name}

+
+
+
+ ) })} -
- - )} + + )} - {/* List of views belonging to selected schema */} - {filteredViews.length > 0 && ( - - -

Views

-

({filteredViews.length})

-
- } - /> + {/* List of foreign tables belonging to selected schema */} + {filteredForeignTables.length > 0 && ( + + +

Foreign Tables

+

+ ({filteredForeignTables.length}) +

+ + } + /> - {filteredViews.map((view: SchemaView) => { - const viewId = Base64.encode(JSON.stringify(view)) - const isActive = id === viewId - return ( - - - -
-

{view.name}

-
-
-
- - ) - })} -
- )} - - {/* List of foreign tables belonging to selected schema */} - {filteredForeignTables.length > 0 && ( - - -

Foreign Tables

-

- ({filteredForeignTables.length}) -

- - } - /> - - {filteredForeignTables.map((table: Partial) => { - const isActive = Number(id) === table.id - return ( - - - -
-

{table.name}

-
-
-
- - ) - })} -
- )} - + {filteredForeignTables.map((table: Partial) => { + const isActive = Number(id) === table.id + return ( + + + +
+

{table.name}

+
+
+
+ + ) + })} + + )} + + )} {searchText.length > 0 && filteredTables.length === 0 && filteredViews.length === 0 && (
@@ -379,13 +386,6 @@ const TableEditorMenu: FC = ({

)} - - {searchText.length === 0 && filteredTables.length === 0 && filteredViews.length === 0 && ( -
-

No tables available

-

This schema has no tables available yet

-
- )} ) } diff --git a/studio/data/fdw/fdw-create-mutation.ts b/studio/data/fdw/fdw-create-mutation.ts index f42a81f2d4d..5ebd1fb6893 100644 --- a/studio/data/fdw/fdw-create-mutation.ts +++ b/studio/data/fdw/fdw-create-mutation.ts @@ -1,40 +1,44 @@ import { useMutation, UseMutationOptions, useQueryClient } from '@tanstack/react-query' -import { AvailableColumn, Wrapper } from 'components/interfaces/Database/Wrappers/Wrappers.types' +import { + AvailableColumn, + WrapperMeta, +} from 'components/interfaces/Database/Wrappers/Wrappers.types' import { executeSql } from 'data/sql/execute-sql-query' import { sqlKeys } from 'data/sql/keys' +import { wrapWithTransaction } from 'data/sql/utils/transaction' import { useStore } from 'hooks' export type FDWCreateVariables = { projectRef?: string connectionString?: string - wrapper: Wrapper + wrapperMeta: WrapperMeta formState: { [k: string]: string } - newTables: any[] + tables: any[] } -export function getFDWCreateSql({ - wrapper, +export function getCreateFDWSql({ + wrapperMeta, formState, - newTables, -}: Pick) { - const newSchemasSql = newTables + tables, +}: Pick) { + const newSchemasSql = tables .filter((table) => table.is_new_schema) .map((table) => /* SQL */ `create schema if not exists ${table.schema_name};`) .join('\n') const createWrapperSql = /* SQL */ ` - create foreign data wrapper ${wrapper.name} - handler ${wrapper.handlerName} - validator ${wrapper.validatorName}; + create foreign data wrapper ${formState.wrapper_name} + handler ${wrapperMeta.handlerName} + validator ${wrapperMeta.validatorName}; ` - const encryptedOptions = wrapper.server.options.filter((option) => option.encrypted) - const unencryptedOptions = wrapper.server.options.filter((option) => !option.encrypted) + const encryptedOptions = wrapperMeta.server.options.filter((option) => option.encrypted) + const unencryptedOptions = wrapperMeta.server.options.filter((option) => !option.encrypted) const createEncryptedKeysSqlArray = encryptedOptions.map((option) => { - const key = `${wrapper.name}_${option.name}` + const key = `${formState.wrapper_name}_${option.name}` // Escape single quotes in postgresql by doubling them up const value = (formState[option.name] || '').replace(/'/g, "''") @@ -67,13 +71,13 @@ export function getFDWCreateSql({ ${encryptedOptions .map( (option) => - /* SQL */ `select id into v_${option.name} from pgsodium.valid_key where name = '${wrapper.name}_${option.name}' limit 1;` + /* SQL */ `select id into v_${option.name} from pgsodium.valid_key where name = '${formState.wrapper_name}_${option.name}' limit 1;` ) .join('\n')} execute format( - E'create server ${wrapper.server.name}\\n' - ' foreign data wrapper ${wrapper.name}\\n' + E'create server ${formState.server_name}\\n' + ' foreign data wrapper ${formState.wrapper_name}\\n' ' options (\\n' ' ${optionsSqlArray}\\n' ' );', @@ -82,9 +86,9 @@ export function getFDWCreateSql({ end $$; ` - const createTablesSql = newTables + const createTablesSql = tables .map((newTable) => { - const table = wrapper.tables[newTable.index] + const table = wrapperMeta.tables[newTable.index] const columns: AvailableColumn[] = newTable.columns .map((name: string) => table.availableColumns.find((c) => c.name === name)) @@ -94,7 +98,7 @@ export function getFDWCreateSql({ create foreign table ${newTable.schema_name}.${newTable.table_name} ( ${columns.map((column) => `${column.name} ${column.type}`).join(',\n ')} ) - server ${wrapper.server.name} + server ${formState.server_name} options ( ${Object.entries(newTable) .filter( @@ -112,8 +116,6 @@ export function getFDWCreateSql({ .join('\n\n') const sql = /* SQL */ ` - begin; - ${newSchemasSql} ${createWrapperSql} @@ -123,8 +125,6 @@ export function getFDWCreateSql({ ${createServerSql} ${createTablesSql} - - commit; ` return sql @@ -133,11 +133,11 @@ export function getFDWCreateSql({ export async function createFDW({ projectRef, connectionString, - wrapper, + wrapperMeta, formState, - newTables, + tables, }: FDWCreateVariables) { - const sql = getFDWCreateSql({ wrapper, formState, newTables }) + const sql = wrapWithTransaction(getCreateFDWSql({ wrapperMeta, formState, tables })) const { result } = await executeSql({ projectRef, connectionString, sql }) @@ -158,7 +158,7 @@ export const useFDWCreateMutation = ({ const { projectRef } = variables await Promise.all([ - queryClient.invalidateQueries(sqlKeys.query(projectRef, ['fdws'])), + queryClient.invalidateQueries(sqlKeys.query(projectRef, ['fdws']), { refetchType: 'all' }), vault.load(), ]) diff --git a/studio/data/fdw/fdw-delete-mutation.ts b/studio/data/fdw/fdw-delete-mutation.ts index 8ecc603a853..d3183fa1fe7 100644 --- a/studio/data/fdw/fdw-delete-mutation.ts +++ b/studio/data/fdw/fdw-delete-mutation.ts @@ -1,17 +1,23 @@ import { useMutation, UseMutationOptions, useQueryClient } from '@tanstack/react-query' -import { Wrapper } from 'components/interfaces/Database/Wrappers/Wrappers.types' +import { WrapperMeta } from 'components/interfaces/Database/Wrappers/Wrappers.types' import { executeSql } from 'data/sql/execute-sql-query' import { sqlKeys } from 'data/sql/keys' +import { wrapWithTransaction } from 'data/sql/utils/transaction' import { useStore } from 'hooks' +import { FDW } from './fdws-query' export type FDWDeleteVariables = { projectRef?: string connectionString?: string - wrapper: Wrapper + wrapper: FDW + wrapperMeta: WrapperMeta } -export const getDeleteFDWSql = ({ wrapper }: Pick) => { - const encryptedOptions = wrapper.server.options.filter((option) => option.encrypted) +export const getDeleteFDWSql = ({ + wrapper, + wrapperMeta, +}: Pick) => { + const encryptedOptions = wrapperMeta.server.options.filter((option) => option.encrypted) const deleteEncryptedSecretsSqlArray = encryptedOptions.map((option) => { const key = `${wrapper.name}_${option.name}` @@ -26,24 +32,25 @@ export const getDeleteFDWSql = ({ wrapper }: Pick const deleteEncryptedSecretsSql = deleteEncryptedSecretsSqlArray.join('\n') const sql = /* SQL */ ` - begin; - drop foreign data wrapper if exists ${wrapper.name} cascade; ${deleteEncryptedSecretsSql} - - commit; ` return sql } -export async function deleteFDW({ projectRef, connectionString, wrapper }: FDWDeleteVariables) { +export async function deleteFDW({ + projectRef, + connectionString, + wrapper, + wrapperMeta, +}: FDWDeleteVariables) { if (!projectRef) { throw new Error('projectRef is required') } - const sql = getDeleteFDWSql({ wrapper }) + const sql = wrapWithTransaction(getDeleteFDWSql({ wrapper, wrapperMeta })) const { result } = await executeSql({ projectRef, connectionString, sql }) diff --git a/studio/data/fdw/fdw-update-mutation.ts b/studio/data/fdw/fdw-update-mutation.ts new file mode 100644 index 00000000000..8e7df030945 --- /dev/null +++ b/studio/data/fdw/fdw-update-mutation.ts @@ -0,0 +1,81 @@ +import { useMutation, UseMutationOptions, useQueryClient } from '@tanstack/react-query' +import { WrapperMeta } from 'components/interfaces/Database/Wrappers/Wrappers.types' +import { executeSql } from 'data/sql/execute-sql-query' +import { sqlKeys } from 'data/sql/keys' +import { wrapWithTransaction } from 'data/sql/utils/transaction' +import { useStore } from 'hooks' +import { getCreateFDWSql } from './fdw-create-mutation' +import { getDeleteFDWSql } from './fdw-delete-mutation' +import { FDW } from './fdws-query' + +export type FDWUpdateVariables = { + projectRef?: string + connectionString?: string + wrapper: FDW + wrapperMeta: WrapperMeta + formState: { + [k: string]: string + } + tables: any[] +} + +export const getUpdateFDWSql = ({ + wrapper, + wrapperMeta, + formState, + tables, +}: Pick) => { + const deleteWrapperSql = getDeleteFDWSql({ wrapper, wrapperMeta }) + const createWrapperSql = getCreateFDWSql({ wrapperMeta, formState, tables }) + + const sql = /* SQL */ ` + ${deleteWrapperSql} + + ${createWrapperSql} + ` + + return sql +} + +export async function updateFDW({ + projectRef, + connectionString, + wrapper, + wrapperMeta, + formState, + tables, +}: FDWUpdateVariables) { + if (!projectRef) { + throw new Error('projectRef is required') + } + + const sql = wrapWithTransaction(getUpdateFDWSql({ wrapper, wrapperMeta, formState, tables })) + + const { result } = await executeSql({ projectRef, connectionString, sql }) + + return result +} + +type FDWUpdateData = Awaited> + +export const useFDWUpdateMutation = ({ + onSuccess, + ...options +}: Omit, 'mutationFn'> = {}) => { + const queryClient = useQueryClient() + const { vault } = useStore() + + return useMutation((vars) => updateFDW(vars), { + async onSuccess(data, variables, context) { + const { projectRef } = variables + + await Promise.all([ + queryClient.invalidateQueries(sqlKeys.query(projectRef, ['fdws'])), + vault.load(), + ]) + + await onSuccess?.(data, variables, context) + }, + ...options, + }) +} diff --git a/studio/data/fdw/fdws-query.ts b/studio/data/fdw/fdws-query.ts index dbcc5f432df..0208805de29 100644 --- a/studio/data/fdw/fdws-query.ts +++ b/studio/data/fdw/fdws-query.ts @@ -5,11 +5,14 @@ export const getFDWsSql = () => { const sql = /* SQL */ ` select s.oid as "id", - s.srvname as "name", + w.fdwname as "name", + s.srvname as "server_name", + s.srvoptions as "server_options", + c.proname as "handler", ( select jsonb_agg( jsonb_build_object( - 'id', c.oid, + 'id', c.oid::bigint, 'schema', relnamespace::regnamespace::text, 'name', c.relname, 'columns', ( @@ -21,13 +24,17 @@ export const getFDWsSql = () => { ) from pg_catalog.pg_attribute a where a.attrelid = c.oid and a.attnum > 0 and not a.attisdropped - ) + ), + 'options', t.ftoptions ) ) from pg_catalog.pg_class c + join pg_catalog.pg_foreign_table t on c.oid = t.ftrelid where c.oid = any (select t.ftrelid from pg_catalog.pg_foreign_table t where t.ftserver = s.oid) ) as "tables" - from pg_catalog.pg_foreign_server s; + from pg_catalog.pg_foreign_server s + join pg_catalog.pg_foreign_data_wrapper w on s.srvfdw = w.oid + join pg_catalog.pg_proc c on w.fdwhandler = c.oid; ` return sql @@ -41,12 +48,17 @@ export type FDWColumn = { export type FDWTable = { id: string name: string + schema: string columns: FDWColumn[] + options: string[] } export type FDW = { - id: string + id: number name: string + handler: string + server_name: string + server_options: string[] tables: FDWTable[] } diff --git a/studio/data/sql/utils/transaction.ts b/studio/data/sql/utils/transaction.ts new file mode 100644 index 00000000000..36fa24b2571 --- /dev/null +++ b/studio/data/sql/utils/transaction.ts @@ -0,0 +1,9 @@ +export function wrapWithTransaction(sql: string) { + return /* SQL */ ` + begin; + + ${sql} + + commit; + ` +} diff --git a/studio/hooks/index.ts b/studio/hooks/index.ts index 53628ed64e5..587af606a57 100644 --- a/studio/hooks/index.ts +++ b/studio/hooks/index.ts @@ -1,6 +1,7 @@ export * from './misc/useStore' export * from './misc/withAuth' export * from './misc/useOptimisticSqlSnippetCreate' +export * from './misc/useImmutableValue' export * from './misc/useNotifications' export * from './misc/usePlatformStatus' export * from './misc/useParams' diff --git a/studio/hooks/misc/useImmutableValue.tsx b/studio/hooks/misc/useImmutableValue.tsx new file mode 100644 index 00000000000..433e0099fef --- /dev/null +++ b/studio/hooks/misc/useImmutableValue.tsx @@ -0,0 +1,14 @@ +import { useRef } from 'react' + +/** + * Once a non-undefined value is passed, it will never change. + */ +export function useImmutableValue(value: T | undefined): T | undefined { + const ref = useRef(value) + + if (!ref.current && value) { + ref.current = value + } + + return ref.current +} diff --git a/studio/package.json b/studio/package.json index b579303f7a3..c4792cb5292 100644 --- a/studio/package.json +++ b/studio/package.json @@ -30,8 +30,8 @@ "@supabase/react-data-grid": "^7.1.0-beta.7", "@supabase/shared-types": "^0.1.29", "@supabase/supabase-js": "^2.1.1", - "@tanstack/react-query": "^4.16.1", - "@tanstack/react-query-devtools": "^4.16.1", + "@tanstack/react-query": "^4.22.0", + "@tanstack/react-query-devtools": "^4.22.0", "@zip.js/zip.js": "^2.6.61", "ajv": "^8.6.3", "analytics-node": "^3.5.0", diff --git a/studio/pages/project/[ref]/database/wrappers/[id].tsx b/studio/pages/project/[ref]/database/wrappers/[id].tsx new file mode 100644 index 00000000000..a5abeb5df61 --- /dev/null +++ b/studio/pages/project/[ref]/database/wrappers/[id].tsx @@ -0,0 +1,21 @@ +import { observer } from 'mobx-react-lite' +import { PermissionAction } from '@supabase/shared-types/out/constants' + +import { NextPageWithLayout } from 'types' +import { checkPermissions } from 'hooks' +import { DatabaseLayout } from 'components/layouts' +import NoPermission from 'components/ui/NoPermission' +import { EditWrapper } from 'components/interfaces/Database' + +const DatabaseWrappersNew: NextPageWithLayout = () => { + const canReadWrappers = checkPermissions(PermissionAction.TENANT_SQL_ADMIN_READ, 'wrappers') + if (!canReadWrappers) { + return + } + + return +} + +DatabaseWrappersNew.getLayout = (page) => {page} + +export default observer(DatabaseWrappersNew) diff --git a/studio/pages/project/[ref]/database/wrappers.tsx b/studio/pages/project/[ref]/database/wrappers/index.tsx similarity index 100% rename from studio/pages/project/[ref]/database/wrappers.tsx rename to studio/pages/project/[ref]/database/wrappers/index.tsx diff --git a/studio/pages/project/[ref]/database/wrappers/new.tsx b/studio/pages/project/[ref]/database/wrappers/new.tsx new file mode 100644 index 00000000000..4db0245131f --- /dev/null +++ b/studio/pages/project/[ref]/database/wrappers/new.tsx @@ -0,0 +1,21 @@ +import { observer } from 'mobx-react-lite' +import { PermissionAction } from '@supabase/shared-types/out/constants' + +import { NextPageWithLayout } from 'types' +import { checkPermissions } from 'hooks' +import { DatabaseLayout } from 'components/layouts' +import NoPermission from 'components/ui/NoPermission' +import { CreateWrapper } from 'components/interfaces/Database' + +const DatabaseWrappersNew: NextPageWithLayout = () => { + const canReadWrappers = checkPermissions(PermissionAction.TENANT_SQL_ADMIN_READ, 'wrappers') + if (!canReadWrappers) { + return + } + + return +} + +DatabaseWrappersNew.getLayout = (page) => {page} + +export default observer(DatabaseWrappersNew) diff --git a/studio/pages/project/[ref]/editor/[id].tsx b/studio/pages/project/[ref]/editor/[id].tsx index e273343bcd9..32893d76bec 100644 --- a/studio/pages/project/[ref]/editor/[id].tsx +++ b/studio/pages/project/[ref]/editor/[id].tsx @@ -7,17 +7,16 @@ import { isUndefined, isNaN } from 'lodash' import { Alert, Button, Checkbox, IconExternalLink, Modal } from 'ui' import { PostgresTable, PostgresColumn } from '@supabase/postgres-meta' -import Base64 from 'lib/base64' -import { tryParseJson } from 'lib/helpers' -import { useStore, withAuth, useUrlState } from 'hooks' +import { useStore, withAuth, useUrlState, useParams } from 'hooks' import { Dictionary } from 'components/grid' import { TableEditorLayout } from 'components/layouts' import { TableGridEditor } from 'components/interfaces' import ConfirmationModal from 'components/ui/ConfirmationModal' +import { SchemaView } from 'types' const TableEditorPage: NextPage = () => { const router = useRouter() - const { id }: any = router.query + const { id } = useParams() const [_, setParams] = useUrlState({ arrayKeys: ['filter', 'sort'] }) const { meta, ui } = useStore() @@ -37,13 +36,17 @@ const TableEditorPage: NextPage = () => { const projectRef = ui.selectedProject?.ref const tables: PostgresTable[] = meta.tables.list() + const views: SchemaView[] = meta.views.list() const foreignTables: Partial[] = meta.foreignTables.list() const selectedTable = !isNaN(Number(id)) ? // @ts-ignore - tables.concat(foreignTables).find((table) => table.id === Number(id)) - : id !== undefined - ? tryParseJson(Base64.decode(id)) + tables + // @ts-ignore + .concat(views) + // @ts-ignore + .concat(foreignTables) + .find((table) => table.id === Number(id)) : undefined useEffect(() => { @@ -140,7 +143,7 @@ const TableEditorPage: NextPage = () => { }) await meta.tables.loadById(selectedColumnToDelete!.table_id) - if (selectedSchema) await meta.schemas.loadViews(selectedSchema) + if (selectedSchema) await meta.views.loadBySchema(selectedSchema) } catch (error: any) { ui.setNotification({ category: 'error', @@ -171,7 +174,7 @@ const TableEditorPage: NextPage = () => { category: 'success', message: `Successfully deleted table "${selectedTableToDelete.name}"`, }) - if (selectedSchema) await meta.schemas.loadViews(selectedSchema) + if (selectedSchema) await meta.views.loadBySchema(selectedSchema) } catch (error: any) { ui.setNotification({ error, diff --git a/studio/pages/project/[ref]/editor/index.tsx b/studio/pages/project/[ref]/editor/index.tsx index 7532dd7fc3c..ee5e4ffc1cf 100644 --- a/studio/pages/project/[ref]/editor/index.tsx +++ b/studio/pages/project/[ref]/editor/index.tsx @@ -55,7 +55,7 @@ const Editor: NextPage = () => { category: 'success', message: `Successfully deleted ${selectedTableToDelete!.name}`, }) - await meta.schemas.loadViews(selectedSchema) + await meta.views.loadBySchema(selectedSchema) } catch (error: any) { ui.setNotification({ category: 'error', diff --git a/studio/stores/common/PostgresMetaInterface.ts b/studio/stores/common/PostgresMetaInterface.ts index 5345022f277..07445b75e46 100644 --- a/studio/stores/common/PostgresMetaInterface.ts +++ b/studio/stores/common/PostgresMetaInterface.ts @@ -15,6 +15,7 @@ export interface IPostgresMetaInterface { isInitialized: boolean load: () => void + loadBySchema: (schema: string) => Promise create: (payload: any) => Promise update: (id: number | string, updates: any) => Promise del: (id: number | string, cascade?: boolean) => Promise @@ -116,6 +117,33 @@ export default class PostgresMetaInterface implements IPostgresMetaInterface< } } + async loadBySchema(schema: string) { + let { LOADING, ERROR, LOADED } = this.STATES + try { + this.setError(null) + this.setState(LOADING) + + const url = this.url.includes('?') + ? `${this.url}&included_schemas=${schema}` + : `${this.url}?included_schemas=${schema}` + const response = await get(url, { headers: this.headers }) + if (response.error) throw response.error + + const data = response as T[] + const formattedData = keyBy(data, this.identifier) + + this.data = { ...this.data, ...formattedData } + this.setState(LOADED) + + return data + } catch (error: any) { + console.error('Error in loadBySchema:', error.message) + this.setError(error) + this.setState(ERROR) + return { error } + } + } + initialDataArray(value: T[]) { if (this.state === this.STATES.INITIAL) { this.data = keyBy(value, this.identifier) diff --git a/studio/stores/pgmeta/ExtensionsStore.ts b/studio/stores/pgmeta/ExtensionsStore.ts index 9df35a1ab4e..fbb26f7d5e8 100644 --- a/studio/stores/pgmeta/ExtensionsStore.ts +++ b/studio/stores/pgmeta/ExtensionsStore.ts @@ -13,4 +13,9 @@ export default class ExtensionsStore extends PostgresMetaInterface - schemas: ISchemaStore + schemas: IPostgresMetaInterface + views: IPostgresMetaInterface foreignTables: IPostgresMetaInterface> hooks: IPostgresMetaInterface @@ -132,6 +135,7 @@ export default class MetaStore implements IMetaStore { tables: TableStore columns: ColumnStore schemas: SchemaStore + views: ViewStore foreignTables: ForeignTableStore hooks: HooksStore @@ -184,6 +188,7 @@ export default class MetaStore implements IMetaStore { this.tables = new TableStore(this.rootStore, `${this.baseUrl}/tables`, this.headers) this.columns = new ColumnStore(this.rootStore, `${this.baseUrl}/columns`, this.headers) this.schemas = new SchemaStore(this.rootStore, `${this.baseUrl}/schemas`, this.headers) + this.views = new ViewStore(this.rootStore, `${this.baseUrl}/views`, this.headers) this.foreignTables = new ForeignTableStore( this.rootStore, `${this.baseUrl}/foreign-tables`, @@ -903,6 +908,9 @@ export default class MetaStore implements IMetaStore { this.schemas.setUrl(`${this.baseUrl}/schemas`) this.schemas.setHeaders(this.headers) + this.views.setUrl(`${this.baseUrl}/views`) + this.views.setHeaders(this.headers) + this.foreignTables.setUrl(`${this.baseUrl}/foreign-tables`) this.foreignTables.setHeaders(this.headers) diff --git a/studio/stores/pgmeta/PublicationStore.ts b/studio/stores/pgmeta/PublicationStore.ts index b892d15057d..86b5cf25642 100644 --- a/studio/stores/pgmeta/PublicationStore.ts +++ b/studio/stores/pgmeta/PublicationStore.ts @@ -12,4 +12,9 @@ export default class PublicationStore extends PostgresMetaInterface { ) { super(rootStore, dataUrl, headers, options) } + + // loadBySchema is not supported in this store + async loadBySchema(schema: string) { + return [] + } } diff --git a/studio/stores/pgmeta/RolesStore.ts b/studio/stores/pgmeta/RolesStore.ts index 5bd5d59fc1b..b4bb4d0c055 100644 --- a/studio/stores/pgmeta/RolesStore.ts +++ b/studio/stores/pgmeta/RolesStore.ts @@ -28,4 +28,9 @@ export default class RolesStore extends PostgresMetaInterface { 'pg_read_all_data', 'pg_write_all_data', ] + + // loadBySchema is not supported in this store + async loadBySchema(schema: string) { + return [] + } } diff --git a/studio/stores/pgmeta/SchemaStore.ts b/studio/stores/pgmeta/SchemaStore.ts index 1fd1ff89ac5..09236d13166 100644 --- a/studio/stores/pgmeta/SchemaStore.ts +++ b/studio/stores/pgmeta/SchemaStore.ts @@ -1,17 +1,8 @@ -import { observable, makeObservable } from 'mobx' import { PostgresSchema } from '@supabase/postgres-meta' - -import PostgresMetaInterface, { IPostgresMetaInterface } from '../common/PostgresMetaInterface' +import PostgresMetaInterface from '../common/PostgresMetaInterface' import { IRootStore } from '../RootStore' -import { SchemaView, ResponseError } from 'types' -export interface ISchemaStore extends IPostgresMetaInterface { - views: SchemaView[] - loadViews: (schema: string) => Promise -} export default class SchemaStore extends PostgresMetaInterface { - views: SchemaView[] = [] - constructor( rootStore: IRootStore, dataUrl: string, @@ -21,10 +12,6 @@ export default class SchemaStore extends PostgresMetaInterface { options?: { identifier: string } ) { super(rootStore, dataUrl, headers, options) - makeObservable(this, { - views: observable, - }) - this.views = [] } // Dashboard to hide schemas with pg_ prefix @@ -34,15 +21,4 @@ export default class SchemaStore extends PostgresMetaInterface { return super.list(schemasFilter) } - - async loadViews(schema: string) { - const query = ` - select schemaname as schema, viewname as name from pg_catalog.pg_views - where schemaname = '${schema}' - order by schemaname, viewname; - ` - const views = await this.rootStore.meta.query(query) - this.views = views - return views - } } diff --git a/studio/stores/pgmeta/ViewStore.ts b/studio/stores/pgmeta/ViewStore.ts new file mode 100644 index 00000000000..63b74701e42 --- /dev/null +++ b/studio/stores/pgmeta/ViewStore.ts @@ -0,0 +1,16 @@ +import { SchemaView } from 'types' +import PostgresMetaInterface from '../common/PostgresMetaInterface' +import { IRootStore } from '../RootStore' + +export default class ViewStore extends PostgresMetaInterface { + constructor( + rootStore: IRootStore, + dataUrl: string, + headers?: { + [prop: string]: any + }, + options?: { identifier: string } + ) { + super(rootStore, dataUrl, headers, options) + } +} diff --git a/studio/stores/project/VaultStore.ts b/studio/stores/project/VaultStore.ts index a470a864470..166f59bf7fe 100644 --- a/studio/stores/project/VaultStore.ts +++ b/studio/stores/project/VaultStore.ts @@ -1,7 +1,7 @@ import { PostgresColumn } from '@supabase/postgres-meta' import { Query } from 'components/grid' import { makeAutoObservable } from 'mobx' -import { VaultSecret } from 'types' +import { SchemaView, VaultSecret } from 'types' import { IRootStore } from '../RootStore' export interface IVaultStore { @@ -211,9 +211,9 @@ export default class VaultStore implements IVaultStore { async listEncryptedColumns(schema: string, table: string) { if (!table) return [] - await this.rootStore.meta.schemas.loadViews(schema) - const decryptedView = this.rootStore.meta.schemas.views.find( - (view) => view.name === `decrypted_${table}` + await this.rootStore.meta.views.loadBySchema(schema) + const decryptedView = this.rootStore.meta.views.find( + (view: SchemaView) => view.name === `decrypted_${table}` ) if (!decryptedView) return [] diff --git a/studio/types/ui.ts b/studio/types/ui.ts index f8d5308ac25..edeb42ec9ac 100644 --- a/studio/types/ui.ts +++ b/studio/types/ui.ts @@ -1,3 +1,5 @@ +import { PostgresColumn } from '@supabase/postgres-meta' + export interface Notification { category: 'info' | 'error' | 'success' | 'loading' message: string // Readable message for users to understand @@ -33,6 +35,10 @@ export interface VaultSecret { } export interface SchemaView { + id: number name: string schema: string + is_updatable: boolean + comment?: string + columns: PostgresColumn[] }