mirror of
https://github.com/supabase/supabase.git
synced 2026-10-05 09:25:06 +03:00
Merge branch 'master' into docs-tabs
This commit is contained in:
105 files changed
+3055
-1211
No files matched your search
@@ -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: [] },
|
||||
],
|
||||
|
||||
@@ -16,7 +16,7 @@ interface ICommonFunc {
|
||||
}
|
||||
|
||||
interface IRefFunctionSection {
|
||||
funcData: any
|
||||
funcData: ICommonFunc
|
||||
commonFuncData: ICommonFunc
|
||||
spec: any
|
||||
}
|
||||
|
||||
@@ -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<IRefFunctionSection> = (props) => {
|
||||
const item = props.spec.functions.find((x: any) => x.id === props.funcData.id)
|
||||
|
||||
@@ -1,35 +1,31 @@
|
||||
import { MDXRemote } from 'next-mdx-remote'
|
||||
import { useRouter } from 'next/router'
|
||||
import { useEffect } from 'react'
|
||||
|
||||
import components from '~/components'
|
||||
import RefEducationSection from '~/components/reference/RefEducationSection'
|
||||
import RefFunctionSection from '~/components/reference/RefFunctionSection'
|
||||
|
||||
import RefSubLayout from '~/layouts/ref/RefSubLayout'
|
||||
import ApiOperationSection from './ApiOperationSection'
|
||||
import CliCommandSection from './CLICommandSection'
|
||||
import { IAPISpec, ICommonFunc, IRefStaticDoc, ISpec, TypeSpec } from './Reference.types'
|
||||
|
||||
interface Props {
|
||||
sections: any[] // to do
|
||||
spec: any // to do
|
||||
typeSpec: any // to do
|
||||
pageProps: any // to do, from staticProps
|
||||
|
||||
interface RefSectionHandlerProps {
|
||||
sections: ICommonFunc[]
|
||||
spec?: ISpec | IAPISpec
|
||||
typeSpec?: TypeSpec
|
||||
pageProps: { docs: IRefStaticDoc[] }
|
||||
type: 'client-lib' | 'cli' | 'api'
|
||||
}
|
||||
|
||||
const RefSectionHandler = (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()
|
||||
}
|
||||
})
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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 }) {
|
||||
|
||||
@@ -139,7 +139,7 @@ Middleware runs before every route declared in the `matcher` array. Since we don
|
||||
>
|
||||
<TabPanel id="js" label="JavaScript">
|
||||
|
||||
Create a new file at `/pages/middleware.js` and populate with the following:
|
||||
Create a new file at `/app/middleware.js` and populate with the following:
|
||||
|
||||
```jsx title="middleware.js"
|
||||
import { createMiddlewareSupabaseClient } from '@supabase/auth-helpers-nextjs'
|
||||
@@ -166,7 +166,7 @@ export const config = {
|
||||
|
||||
<TabPanel id="ts" label="TypeScript">
|
||||
|
||||
Create a new file at `/middleware.ts` (a sibling of /pages) and populate with the following:
|
||||
Create a new file at `/app/middleware.ts` and populate with the following:
|
||||
|
||||
```tsx title="middleware.ts"
|
||||
import { createMiddlewareSupabaseClient } from '@supabase/auth-helpers-nextjs'
|
||||
@@ -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.reload()
|
||||
router.refresh()
|
||||
}
|
||||
})
|
||||
|
||||
return () => subscription.unsubscribe()
|
||||
}, [accessToken])
|
||||
|
||||
return null
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
<source src="/docs/videos/auth-zoom2.mp4" type="video/mp4" muted playsInline />
|
||||
</video>
|
||||
|
||||
export const Page = ({ children }) => {
|
||||
const router = useRouter()
|
||||
if (process.env.NEXT_PUBLIC_NEW_DOCS === 'true' && !router.asPath.includes('overview')) {
|
||||
return NewContent()
|
||||
}
|
||||
return <Layout meta={meta} children={children} />
|
||||
}
|
||||
export const Page = ({ children }) => <Layout meta={meta} children={children} />
|
||||
|
||||
export default Page
|
||||
@@ -95,9 +95,9 @@ You can insert geographical data through SQL or through our API.
|
||||
insert into public.restaurants
|
||||
(name, location)
|
||||
values
|
||||
('Supa Burger', st_point(-73.946823 40.807416)),
|
||||
('Supa Pizza', st_point(-73.94581 40.807475)),
|
||||
('Supa Taco', st_point(-73.945826 40.80629));
|
||||
('Supa Burger', st_point(-73.946823, 40.807416)),
|
||||
('Supa Pizza', st_point(-73.94581, 40.807475)),
|
||||
('Supa Taco', st_point(-73.945826, 40.80629));
|
||||
```
|
||||
|
||||
</TabPanel>
|
||||
@@ -143,7 +143,7 @@ await supabase.from('restaurants').insert([
|
||||
</TabPanel>
|
||||
</Tabs>
|
||||
|
||||
Notice the order in which you pass the latitude and longitude. Longitude comes first, and is because longitude represents the x-axis of the location. Another thing to watch for is that there is no comma between the two values, just a single space.
|
||||
Notice the order in which you pass the latitude and longitude. Longitude comes first, and is because longitude represents the x-axis of the location. Another thing to watch for is when inserting data from the client library, there is no comma between the two values, just a single space.
|
||||
|
||||
At this point, if you go into your Supabase dashboard and look at the data, you will notice that the value of the `location` column looks something like this.
|
||||
|
||||
|
||||
@@ -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');
|
||||
```
|
||||
|
||||
</TabPanel>
|
||||
@@ -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'");
|
||||
```
|
||||
|
||||
</TabPanel>
|
||||
@@ -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'");
|
||||
```
|
||||
|
||||
</TabPanel>
|
||||
@@ -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'");
|
||||
```
|
||||
|
||||
</TabPanel>
|
||||
@@ -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'");
|
||||
```
|
||||
|
||||
</TabPanel>
|
||||
@@ -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'");
|
||||
```
|
||||
|
||||
</TabPanel>
|
||||
@@ -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'");
|
||||
```
|
||||
|
||||
</TabPanel>
|
||||
@@ -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'");
|
||||
```
|
||||
|
||||
</TabPanel>
|
||||
@@ -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'");
|
||||
```
|
||||
|
||||
</TabPanel>
|
||||
|
||||
@@ -88,9 +88,8 @@ Reference: [rpc()](../../reference/javascript/rpc)
|
||||
<TabPanel id="dart" label="Dart">
|
||||
|
||||
```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)
|
||||
<TabPanel id="dart" label="Dart">
|
||||
|
||||
```dart
|
||||
final res = await supabase
|
||||
final data = await supabase
|
||||
.rpc('get_planets')
|
||||
.eq('id', 1)
|
||||
.execute();
|
||||
.eq('id', 1);
|
||||
```
|
||||
|
||||
</TabPanel>
|
||||
@@ -254,9 +252,8 @@ const { data, error } = await supabase.rpc('add_planet', { name: 'Jakku' })
|
||||
<TabPanel id="dart" label="Dart">
|
||||
|
||||
```dart
|
||||
final res = await supabase
|
||||
.rpc('add_planet', params: { 'name': 'Jakku' })
|
||||
.execute();
|
||||
final data = await supabase
|
||||
.rpc('add_planet', params: { 'name': 'Jakku' });
|
||||
```
|
||||
|
||||
</TabPanel>
|
||||
|
||||
@@ -206,7 +206,7 @@ const { data, error } = await supabase
|
||||
<TabPanel id="dart" label="Dart">
|
||||
|
||||
```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();
|
||||
}]);
|
||||
```
|
||||
|
||||
</TabPanel>
|
||||
|
||||
@@ -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',
|
||||
},
|
||||
]
|
||||
|
||||
@@ -86,7 +86,7 @@ export const meta = {
|
||||
```js lib/supabaseClient.js
|
||||
import { createClient } from '@supabase/supabase-js'
|
||||
|
||||
const supabase = createClient('https://<project>.supabase.co', '<your-anon-key>')
|
||||
export const supabase = createClient('https://<project>.supabase.co', '<your-anon-key>')
|
||||
```
|
||||
|
||||
</StepHikeCompact.Code>
|
||||
|
||||
@@ -56,8 +56,7 @@ import { env } from '$env/dynamic/public'
|
||||
export const supabase = createClient(env.PUBLIC_SUPABASE_URL, env.PUBLIC_SUPABASE_ANON_KEY)
|
||||
```
|
||||
|
||||
And one optional step is to update the CSS file `public/global.css` to make the app look nice.
|
||||
You can find the full contents of this file [here](https://raw.githubusercontent.com/supabase/supabase/master/examples/user-management/svelte-user-management/src/app.css).
|
||||
Optionally, update `src/routes/style.css` with the [CSS from the example](https://raw.githubusercontent.com/supabase/supabase/master/examples/user-management/svelte-user-management/src/app.css).
|
||||
|
||||
### Supabase Auth Helpers
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||

|
||||
|
||||
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`.
|
||||
|
||||

|
||||
|
||||
## 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.
|
||||
|
||||

|
||||
|
||||
### 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.
|
||||
|
||||

|
||||
|
||||
## 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 (
|
||||
<PicketProvider apiKey={process.env.NEXT_PUBLIC_PICKET_PUBLISHABLE_KEY!}>
|
||||
<Component {...pageProps} />
|
||||
</PicketProvider>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
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 (
|
||||
<div className={styles.container}>
|
||||
<main className={styles.main}>
|
||||
{loggedIn ? (
|
||||
<button onClick={handleLogout}>Log Out to Switch Wallets</button>
|
||||
) : (
|
||||
<button onClick={handleLogin}>Log In with Your Wallet</button>
|
||||
)}
|
||||
</main>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export const getServerSideProps: GetServerSideProps<Props> = 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 (
|
||||
<div className={styles.container}>
|
||||
<Head>
|
||||
<title>Picket 💜 Supabase</title>
|
||||
</Head>
|
||||
|
||||
<main className={styles.main}>
|
||||
<h1 className={styles.title}>Your Personal Todo List</h1>
|
||||
<div
|
||||
style={{
|
||||
maxWidth: '600px',
|
||||
textAlign: 'left',
|
||||
fontSize: '1.125rem',
|
||||
margin: '36px 0 24px 0',
|
||||
}}
|
||||
>
|
||||
<p>Welcome {displayWalletAddress(walletAddress)},</p>
|
||||
<p>
|
||||
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.
|
||||
</p>
|
||||
</div>
|
||||
<div
|
||||
style={{
|
||||
textAlign: 'left',
|
||||
fontSize: '1.125rem',
|
||||
}}
|
||||
>
|
||||
<h2>Todo List</h2>
|
||||
{todos.map((todo) => (
|
||||
<div
|
||||
key={todo.name}
|
||||
style={{
|
||||
margin: '8px 0',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
}}
|
||||
>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={todo.completed}
|
||||
onChange={async () => {
|
||||
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))
|
||||
)
|
||||
}}
|
||||
/>
|
||||
<span
|
||||
style={{
|
||||
margin: '0 0 0 8px',
|
||||
}}
|
||||
>
|
||||
{todo.name}
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
<div
|
||||
style={{
|
||||
margin: '24px 0',
|
||||
}}
|
||||
>
|
||||
<Link
|
||||
href={'/'}
|
||||
style={{
|
||||
textDecoration: 'underline',
|
||||
}}
|
||||
>
|
||||
Go back home →
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export const getServerSideProps: GetServerSideProps<Props> = 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 }) => <Layout meta={meta} children={children} />
|
||||
|
||||
export default Page
|
||||
@@ -31,6 +31,13 @@ Make sure you have these installed on your local machine:
|
||||
supabase login
|
||||
```
|
||||
|
||||
<Admonition type="note">
|
||||
|
||||
If you installed the Supabase CLI via NPM you may have to use `npx supabase login`.
|
||||
|
||||
</Admonition>
|
||||
|
||||
|
||||
## Initialize your project
|
||||
|
||||
Create a new folder for your project and start a new git repository:
|
||||
|
||||
@@ -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',
|
||||
},
|
||||
]
|
||||
|
||||
@@ -15,8 +15,8 @@ export default function Config(props) {
|
||||
return <RefSectionHandler sections={sections} spec={generatedSpec} pageProps={props} type="api" />
|
||||
}
|
||||
|
||||
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() {
|
||||
|
||||
@@ -14,8 +14,8 @@ export default function CliRef(props) {
|
||||
return <RefSectionHandler sections={sections} spec={spec} pageProps={props} type="cli" />
|
||||
}
|
||||
|
||||
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() {
|
||||
|
||||
@@ -9,11 +9,11 @@ import handleRefStaticProps from '~/lib/mdx/handleRefStaticProps'
|
||||
const sections = flattenSections(clientLibsCommonSections)
|
||||
|
||||
export default function JSReference(props) {
|
||||
return <RefSectionHandler sections={sections} spec={spec} pageProps={props} />
|
||||
return <RefSectionHandler sections={sections} spec={spec} 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() {
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -9,11 +9,11 @@ import handleRefStaticProps from '~/lib/mdx/handleRefStaticProps'
|
||||
const sections = flattenSections(clientLibsCommonSections)
|
||||
|
||||
export default function JSReference(props) {
|
||||
return <RefSectionHandler sections={sections} spec={spec} pageProps={props} />
|
||||
return <RefSectionHandler sections={sections} spec={spec} 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() {
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -10,11 +10,19 @@ import handleRefStaticProps from '~/lib/mdx/handleRefStaticProps'
|
||||
const sections = flattenSections(clientLibsCommonSections)
|
||||
|
||||
export default function JSReference(props) {
|
||||
return <RefSectionHandler sections={sections} spec={spec} typeSpec={typeSpec} pageProps={props} />
|
||||
return (
|
||||
<RefSectionHandler
|
||||
sections={sections}
|
||||
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() {
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -10,11 +10,19 @@ import handleRefStaticProps from '~/lib/mdx/handleRefStaticProps'
|
||||
const sections = flattenSections(clientLibsCommonSections)
|
||||
|
||||
export default function JSReference(props) {
|
||||
return <RefSectionHandler sections={sections} spec={spec} typeSpec={typeSpec} pageProps={props} />
|
||||
return (
|
||||
<RefSectionHandler
|
||||
sections={sections}
|
||||
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() {
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -12,11 +12,11 @@ const sections = flattenSections(selfHostingAuthCommonSections)
|
||||
const spec = gen_v3(authSpec, 'wat', { apiUrl: 'apiv0' })
|
||||
|
||||
export default function JSReference(props) {
|
||||
return <RefSectionHandler sections={sections} spec={spec} pageProps={props} />
|
||||
return <RefSectionHandler sections={sections} spec={spec} pageProps={props} type="api" />
|
||||
}
|
||||
|
||||
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() {
|
||||
|
||||
@@ -9,11 +9,11 @@ import { gen_v3 } from '~/lib/refGenerator/helpers'
|
||||
const sections = flattenSections(selfHostingRealtimeCommonSections)
|
||||
|
||||
export default function JSReference(props) {
|
||||
return <RefSectionHandler sections={sections} pageProps={props} />
|
||||
return <RefSectionHandler sections={sections} pageProps={props} type="api" />
|
||||
}
|
||||
|
||||
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() {
|
||||
|
||||
@@ -12,11 +12,11 @@ const sections = flattenSections(selfHostingStorageCommonSections)
|
||||
const spec = gen_v3(storageSpec, 'wat', { apiUrl: 'apiv0' })
|
||||
|
||||
export default function JSReference(props) {
|
||||
return <RefSectionHandler sections={sections} spec={spec} pageProps={props} />
|
||||
return <RefSectionHandler sections={sections} spec={spec} pageProps={props} type="api" />
|
||||
}
|
||||
|
||||
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() {
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 219 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 217 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 228 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 166 KiB |
+174
-126
@@ -78,6 +78,12 @@
|
||||
<changefreq>0.5</changefreq>
|
||||
</url>
|
||||
|
||||
<url>
|
||||
<loc>https://supabase.com/docs/guides/self-hosting</loc>
|
||||
<changefreq>weekly</changefreq>
|
||||
<changefreq>0.5</changefreq>
|
||||
</url>
|
||||
|
||||
<url>
|
||||
<loc>https://supabase.com/docs/guides/storage</loc>
|
||||
<changefreq>weekly</changefreq>
|
||||
@@ -126,6 +132,78 @@
|
||||
<changefreq>0.5</changefreq>
|
||||
</url>
|
||||
|
||||
<url>
|
||||
<loc>https://supabase.com/docs/guides/auth/auth-captcha</loc>
|
||||
<changefreq>weekly</changefreq>
|
||||
<changefreq>0.5</changefreq>
|
||||
</url>
|
||||
|
||||
<url>
|
||||
<loc>https://supabase.com/docs/guides/auth/auth-email-templates</loc>
|
||||
<changefreq>weekly</changefreq>
|
||||
<changefreq>0.5</changefreq>
|
||||
</url>
|
||||
|
||||
<url>
|
||||
<loc>https://supabase.com/docs/guides/auth/auth-email</loc>
|
||||
<changefreq>weekly</changefreq>
|
||||
<changefreq>0.5</changefreq>
|
||||
</url>
|
||||
|
||||
<url>
|
||||
<loc>https://supabase.com/docs/guides/auth/auth-helpers</loc>
|
||||
<changefreq>weekly</changefreq>
|
||||
<changefreq>0.5</changefreq>
|
||||
</url>
|
||||
|
||||
<url>
|
||||
<loc>https://supabase.com/docs/guides/auth/auth-magic-link</loc>
|
||||
<changefreq>weekly</changefreq>
|
||||
<changefreq>0.5</changefreq>
|
||||
</url>
|
||||
|
||||
<url>
|
||||
<loc>https://supabase.com/docs/guides/auth/auth-mfa</loc>
|
||||
<changefreq>weekly</changefreq>
|
||||
<changefreq>0.5</changefreq>
|
||||
</url>
|
||||
|
||||
<url>
|
||||
<loc>https://supabase.com/docs/guides/auth/managing-user-data</loc>
|
||||
<changefreq>weekly</changefreq>
|
||||
<changefreq>0.5</changefreq>
|
||||
</url>
|
||||
|
||||
<url>
|
||||
<loc>https://supabase.com/docs/guides/auth/overview</loc>
|
||||
<changefreq>weekly</changefreq>
|
||||
<changefreq>0.5</changefreq>
|
||||
</url>
|
||||
|
||||
<url>
|
||||
<loc>https://supabase.com/docs/guides/auth/phone-login</loc>
|
||||
<changefreq>weekly</changefreq>
|
||||
<changefreq>0.5</changefreq>
|
||||
</url>
|
||||
|
||||
<url>
|
||||
<loc>https://supabase.com/docs/guides/auth/row-level-security</loc>
|
||||
<changefreq>weekly</changefreq>
|
||||
<changefreq>0.5</changefreq>
|
||||
</url>
|
||||
|
||||
<url>
|
||||
<loc>https://supabase.com/docs/guides/auth/server-side-rendering</loc>
|
||||
<changefreq>weekly</changefreq>
|
||||
<changefreq>0.5</changefreq>
|
||||
</url>
|
||||
|
||||
<url>
|
||||
<loc>https://supabase.com/docs/guides/auth/social-login</loc>
|
||||
<changefreq>weekly</changefreq>
|
||||
<changefreq>0.5</changefreq>
|
||||
</url>
|
||||
|
||||
<url>
|
||||
<loc>https://supabase.com/docs/guides/database/arrays</loc>
|
||||
<changefreq>weekly</changefreq>
|
||||
@@ -216,78 +294,6 @@
|
||||
<changefreq>0.5</changefreq>
|
||||
</url>
|
||||
|
||||
<url>
|
||||
<loc>https://supabase.com/docs/guides/auth/auth-captcha</loc>
|
||||
<changefreq>weekly</changefreq>
|
||||
<changefreq>0.5</changefreq>
|
||||
</url>
|
||||
|
||||
<url>
|
||||
<loc>https://supabase.com/docs/guides/auth/auth-email-templates</loc>
|
||||
<changefreq>weekly</changefreq>
|
||||
<changefreq>0.5</changefreq>
|
||||
</url>
|
||||
|
||||
<url>
|
||||
<loc>https://supabase.com/docs/guides/auth/auth-email</loc>
|
||||
<changefreq>weekly</changefreq>
|
||||
<changefreq>0.5</changefreq>
|
||||
</url>
|
||||
|
||||
<url>
|
||||
<loc>https://supabase.com/docs/guides/auth/auth-helpers</loc>
|
||||
<changefreq>weekly</changefreq>
|
||||
<changefreq>0.5</changefreq>
|
||||
</url>
|
||||
|
||||
<url>
|
||||
<loc>https://supabase.com/docs/guides/auth/auth-magic-link</loc>
|
||||
<changefreq>weekly</changefreq>
|
||||
<changefreq>0.5</changefreq>
|
||||
</url>
|
||||
|
||||
<url>
|
||||
<loc>https://supabase.com/docs/guides/auth/auth-mfa</loc>
|
||||
<changefreq>weekly</changefreq>
|
||||
<changefreq>0.5</changefreq>
|
||||
</url>
|
||||
|
||||
<url>
|
||||
<loc>https://supabase.com/docs/guides/auth/managing-user-data</loc>
|
||||
<changefreq>weekly</changefreq>
|
||||
<changefreq>0.5</changefreq>
|
||||
</url>
|
||||
|
||||
<url>
|
||||
<loc>https://supabase.com/docs/guides/auth/overview</loc>
|
||||
<changefreq>weekly</changefreq>
|
||||
<changefreq>0.5</changefreq>
|
||||
</url>
|
||||
|
||||
<url>
|
||||
<loc>https://supabase.com/docs/guides/auth/phone-login</loc>
|
||||
<changefreq>weekly</changefreq>
|
||||
<changefreq>0.5</changefreq>
|
||||
</url>
|
||||
|
||||
<url>
|
||||
<loc>https://supabase.com/docs/guides/auth/row-level-security</loc>
|
||||
<changefreq>weekly</changefreq>
|
||||
<changefreq>0.5</changefreq>
|
||||
</url>
|
||||
|
||||
<url>
|
||||
<loc>https://supabase.com/docs/guides/auth/server-side-rendering</loc>
|
||||
<changefreq>weekly</changefreq>
|
||||
<changefreq>0.5</changefreq>
|
||||
</url>
|
||||
|
||||
<url>
|
||||
<loc>https://supabase.com/docs/guides/auth/social-login</loc>
|
||||
<changefreq>weekly</changefreq>
|
||||
<changefreq>0.5</changefreq>
|
||||
</url>
|
||||
|
||||
<url>
|
||||
<loc>https://supabase.com/docs/guides/functions/auth</loc>
|
||||
<changefreq>weekly</changefreq>
|
||||
@@ -570,12 +576,6 @@
|
||||
<changefreq>0.5</changefreq>
|
||||
</url>
|
||||
|
||||
<url>
|
||||
<loc>https://supabase.com/docs/guides/resources/self-hosting</loc>
|
||||
<changefreq>weekly</changefreq>
|
||||
<changefreq>0.5</changefreq>
|
||||
</url>
|
||||
|
||||
<url>
|
||||
<loc>https://supabase.com/docs/guides/resources/supabase-cli</loc>
|
||||
<changefreq>weekly</changefreq>
|
||||
@@ -612,6 +612,12 @@
|
||||
<changefreq>0.5</changefreq>
|
||||
</url>
|
||||
|
||||
<url>
|
||||
<loc>https://supabase.com/docs/guides/self-hosting/docker</loc>
|
||||
<changefreq>weekly</changefreq>
|
||||
<changefreq>0.5</changefreq>
|
||||
</url>
|
||||
|
||||
<url>
|
||||
<loc>https://supabase.com/docs/learn/auth-deep-dive/auth-deep-dive-jwts</loc>
|
||||
<changefreq>weekly</changefreq>
|
||||
@@ -642,42 +648,6 @@
|
||||
<changefreq>0.5</changefreq>
|
||||
</url>
|
||||
|
||||
<url>
|
||||
<loc>https://supabase.com/docs/guides/database/extensions/http</loc>
|
||||
<changefreq>weekly</changefreq>
|
||||
<changefreq>0.5</changefreq>
|
||||
</url>
|
||||
|
||||
<url>
|
||||
<loc>https://supabase.com/docs/guides/database/extensions/pgcron</loc>
|
||||
<changefreq>weekly</changefreq>
|
||||
<changefreq>0.5</changefreq>
|
||||
</url>
|
||||
|
||||
<url>
|
||||
<loc>https://supabase.com/docs/guides/database/extensions/pgnet</loc>
|
||||
<changefreq>weekly</changefreq>
|
||||
<changefreq>0.5</changefreq>
|
||||
</url>
|
||||
|
||||
<url>
|
||||
<loc>https://supabase.com/docs/guides/database/extensions/pgtap</loc>
|
||||
<changefreq>weekly</changefreq>
|
||||
<changefreq>0.5</changefreq>
|
||||
</url>
|
||||
|
||||
<url>
|
||||
<loc>https://supabase.com/docs/guides/database/extensions/plv8</loc>
|
||||
<changefreq>weekly</changefreq>
|
||||
<changefreq>0.5</changefreq>
|
||||
</url>
|
||||
|
||||
<url>
|
||||
<loc>https://supabase.com/docs/guides/database/extensions/uuid-ossp</loc>
|
||||
<changefreq>weekly</changefreq>
|
||||
<changefreq>0.5</changefreq>
|
||||
</url>
|
||||
|
||||
<url>
|
||||
<loc>https://supabase.com/docs/guides/auth/auth-helpers/auth-ui</loc>
|
||||
<changefreq>weekly</changefreq>
|
||||
@@ -829,25 +799,43 @@
|
||||
</url>
|
||||
|
||||
<url>
|
||||
<loc>https://supabase.com/docs/guides/functions/examples/og-image</loc>
|
||||
<loc>https://supabase.com/docs/guides/database/extensions/http</loc>
|
||||
<changefreq>weekly</changefreq>
|
||||
<changefreq>0.5</changefreq>
|
||||
</url>
|
||||
|
||||
<url>
|
||||
<loc>https://supabase.com/docs/guides/getting-started/quickstarts/nextjs</loc>
|
||||
<loc>https://supabase.com/docs/guides/database/extensions/pgcron</loc>
|
||||
<changefreq>weekly</changefreq>
|
||||
<changefreq>0.5</changefreq>
|
||||
</url>
|
||||
|
||||
<url>
|
||||
<loc>https://supabase.com/docs/guides/getting-started/quickstarts/nuxtjs</loc>
|
||||
<loc>https://supabase.com/docs/guides/database/extensions/pgnet</loc>
|
||||
<changefreq>weekly</changefreq>
|
||||
<changefreq>0.5</changefreq>
|
||||
</url>
|
||||
|
||||
<url>
|
||||
<loc>https://supabase.com/docs/guides/getting-started/quickstarts/reactjs</loc>
|
||||
<loc>https://supabase.com/docs/guides/database/extensions/pgtap</loc>
|
||||
<changefreq>weekly</changefreq>
|
||||
<changefreq>0.5</changefreq>
|
||||
</url>
|
||||
|
||||
<url>
|
||||
<loc>https://supabase.com/docs/guides/database/extensions/plv8</loc>
|
||||
<changefreq>weekly</changefreq>
|
||||
<changefreq>0.5</changefreq>
|
||||
</url>
|
||||
|
||||
<url>
|
||||
<loc>https://supabase.com/docs/guides/database/extensions/postgis</loc>
|
||||
<changefreq>weekly</changefreq>
|
||||
<changefreq>0.5</changefreq>
|
||||
</url>
|
||||
|
||||
<url>
|
||||
<loc>https://supabase.com/docs/guides/database/extensions/uuid-ossp</loc>
|
||||
<changefreq>weekly</changefreq>
|
||||
<changefreq>0.5</changefreq>
|
||||
</url>
|
||||
@@ -936,6 +924,30 @@
|
||||
<changefreq>0.5</changefreq>
|
||||
</url>
|
||||
|
||||
<url>
|
||||
<loc>https://supabase.com/docs/guides/functions/examples/og-image</loc>
|
||||
<changefreq>weekly</changefreq>
|
||||
<changefreq>0.5</changefreq>
|
||||
</url>
|
||||
|
||||
<url>
|
||||
<loc>https://supabase.com/docs/guides/getting-started/quickstarts/nextjs</loc>
|
||||
<changefreq>weekly</changefreq>
|
||||
<changefreq>0.5</changefreq>
|
||||
</url>
|
||||
|
||||
<url>
|
||||
<loc>https://supabase.com/docs/guides/getting-started/quickstarts/nuxtjs</loc>
|
||||
<changefreq>weekly</changefreq>
|
||||
<changefreq>0.5</changefreq>
|
||||
</url>
|
||||
|
||||
<url>
|
||||
<loc>https://supabase.com/docs/guides/getting-started/quickstarts/reactjs</loc>
|
||||
<changefreq>weekly</changefreq>
|
||||
<changefreq>0.5</changefreq>
|
||||
</url>
|
||||
|
||||
<url>
|
||||
<loc>https://supabase.com/docs/guides/platform/sso/azure</loc>
|
||||
<changefreq>weekly</changefreq>
|
||||
@@ -954,6 +966,24 @@
|
||||
<changefreq>0.5</changefreq>
|
||||
</url>
|
||||
|
||||
<url>
|
||||
<loc>https://supabase.com/docs/guides/resources/supabase-cli/local-development</loc>
|
||||
<changefreq>weekly</changefreq>
|
||||
<changefreq>0.5</changefreq>
|
||||
</url>
|
||||
|
||||
<url>
|
||||
<loc>https://supabase.com/docs/guides/resources/supabase-cli/managing-environments</loc>
|
||||
<changefreq>weekly</changefreq>
|
||||
<changefreq>0.5</changefreq>
|
||||
</url>
|
||||
|
||||
<url>
|
||||
<loc>https://supabase.com/docs/guides/self-hosting/auth/config</loc>
|
||||
<changefreq>weekly</changefreq>
|
||||
<changefreq>0.5</changefreq>
|
||||
</url>
|
||||
|
||||
<url>
|
||||
<loc>https://supabase.com/docs/guides/resources/migrating-to-supabase/firebase-auth</loc>
|
||||
<changefreq>weekly</changefreq>
|
||||
@@ -979,19 +1009,13 @@
|
||||
</url>
|
||||
|
||||
<url>
|
||||
<loc>https://supabase.com/docs/guides/resources/self-hosting/docker</loc>
|
||||
<loc>https://supabase.com/docs/guides/self-hosting/realtime/config</loc>
|
||||
<changefreq>weekly</changefreq>
|
||||
<changefreq>0.5</changefreq>
|
||||
</url>
|
||||
|
||||
<url>
|
||||
<loc>https://supabase.com/docs/guides/resources/supabase-cli/local-development</loc>
|
||||
<changefreq>weekly</changefreq>
|
||||
<changefreq>0.5</changefreq>
|
||||
</url>
|
||||
|
||||
<url>
|
||||
<loc>https://supabase.com/docs/guides/resources/supabase-cli/managing-environments</loc>
|
||||
<loc>https://supabase.com/docs/guides/self-hosting/storage/config</loc>
|
||||
<changefreq>weekly</changefreq>
|
||||
<changefreq>0.5</changefreq>
|
||||
</url>
|
||||
@@ -3326,6 +3350,24 @@
|
||||
<changefreq>0.5</changefreq>
|
||||
</url>
|
||||
|
||||
<url>
|
||||
<loc>https://supabase.com/docs/reference/cli/supabase-network-bans</loc>
|
||||
<changefreq>weekly</changefreq>
|
||||
<changefreq>0.5</changefreq>
|
||||
</url>
|
||||
|
||||
<url>
|
||||
<loc>https://supabase.com/docs/reference/cli/supabase-network-bans-remove</loc>
|
||||
<changefreq>weekly</changefreq>
|
||||
<changefreq>0.5</changefreq>
|
||||
</url>
|
||||
|
||||
<url>
|
||||
<loc>https://supabase.com/docs/reference/cli/supabase-network-bans-get</loc>
|
||||
<changefreq>weekly</changefreq>
|
||||
<changefreq>0.5</changefreq>
|
||||
</url>
|
||||
|
||||
<url>
|
||||
<loc>https://supabase.com/docs/reference/cli/supabase-migration</loc>
|
||||
<changefreq>weekly</changefreq>
|
||||
@@ -3488,6 +3530,12 @@
|
||||
<changefreq>0.5</changefreq>
|
||||
</url>
|
||||
|
||||
<url>
|
||||
<loc>https://supabase.com/docs/reference/cli/supabase-db-dump</loc>
|
||||
<changefreq>weekly</changefreq>
|
||||
<changefreq>0.5</changefreq>
|
||||
</url>
|
||||
|
||||
<url>
|
||||
<loc>https://supabase.com/docs/reference/cli/supabase-db-diff</loc>
|
||||
<changefreq>weekly</changefreq>
|
||||
|
||||
@@ -8,6 +8,7 @@ author_image_url: https://avatars0.githubusercontent.com/u/26374889?s=400&u=f5e3
|
||||
authorURL: https://github.com/dragarcia
|
||||
tags:
|
||||
- postgres
|
||||
- planetpg
|
||||
date: '07-09-2020'
|
||||
---
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@ author_image_url: https://avatars0.githubusercontent.com/u/26374889?s=400&u=f5e3
|
||||
authorURL: https://github.com/dragarcia
|
||||
tags:
|
||||
- postgres
|
||||
- planetpg
|
||||
date: '07-07-2020'
|
||||
---
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@ author_image_url: https://avatars0.githubusercontent.com/u/26374889?s=400&u=f5e3
|
||||
authorURL: https://github.com/dragarcia
|
||||
tags:
|
||||
- postgres
|
||||
- planetpg
|
||||
date: '08-02-2020'
|
||||
---
|
||||
|
||||
|
||||
@@ -9,6 +9,7 @@ authorURL: https://github.com/dragarcia
|
||||
image: postgres-views.png
|
||||
tags:
|
||||
- postgres
|
||||
- planetpg
|
||||
date: '11-18-2020'
|
||||
---
|
||||
|
||||
|
||||
@@ -11,6 +11,7 @@ thumb: elephants.png
|
||||
tags:
|
||||
- sql
|
||||
- postgres
|
||||
- planetpg
|
||||
date: '02-27-2021'
|
||||
---
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@ image: supabase-postgres-cron.png
|
||||
thumb: supabase-postgres-cron.png
|
||||
tags:
|
||||
- postgres
|
||||
- planetpg
|
||||
date: '03-05-2021'
|
||||
---
|
||||
|
||||
|
||||
@@ -9,6 +9,7 @@ image: postgres-hooks/og-postgres-hooks.jpg
|
||||
thumb: postgres-hooks/cover-postgres-hooks.jpg
|
||||
tags:
|
||||
- postgres
|
||||
- planetpg
|
||||
date: '2021-07-02'
|
||||
---
|
||||
|
||||
|
||||
@@ -9,6 +9,7 @@ image: supa-audit/postgres_auditing_in_150_lines_of_SQL.png
|
||||
thumb: supa-audit/postgres_auditing_in_150_lines_of_SQL.png
|
||||
tags:
|
||||
- postgres
|
||||
- planetpg
|
||||
date: '2022-03-08'
|
||||
toc_depth: 3
|
||||
---
|
||||
|
||||
@@ -7,6 +7,7 @@ thumb: partial-dumps/og-partial-dumps-with-rls.png
|
||||
tags:
|
||||
- postgresql
|
||||
- data
|
||||
- planetpg
|
||||
date: '2022-06-28'
|
||||
toc_depth: 3
|
||||
---
|
||||
|
||||
@@ -6,6 +6,7 @@ image: seen-by/seen-by-postgresql-thumb.png
|
||||
thumb: seen-by/seen-by-postgresql-thumb.png
|
||||
tags:
|
||||
- postgres
|
||||
- planetpg
|
||||
date: '2022-07-18'
|
||||
toc_depth: 3
|
||||
---
|
||||
|
||||
@@ -6,6 +6,7 @@ image: primary-keys/postgres-primary-key.jpg
|
||||
thumb: primary-keys/postgres-primary-key.jpg
|
||||
tags:
|
||||
- postgres
|
||||
- planetpg
|
||||
date: '2022-09-08'
|
||||
toc_depth: 3
|
||||
---
|
||||
|
||||
@@ -6,6 +6,7 @@ image: postgres-wasm/postgres-wasm.jpg
|
||||
thumb: postgres-wasm/postgres-wasm.jpg
|
||||
tags:
|
||||
- postgres
|
||||
- planetpg
|
||||
date: '2022-10-03'
|
||||
toc_depth: 3
|
||||
---
|
||||
|
||||
@@ -6,6 +6,7 @@ image: postgres-fts-vs-the-rest/postgres-full-text-search.jpg
|
||||
thumb: postgres-fts-vs-the-rest/postgres-full-text-search.jpg
|
||||
tags:
|
||||
- postgres
|
||||
- planetpg
|
||||
date: '2022-10-14'
|
||||
toc_depth: 3
|
||||
---
|
||||
|
||||
@@ -6,6 +6,7 @@ image: what-is-postgres-commitfest/what-is-postgres-commitfest.jpg
|
||||
thumb: what-is-postgres-commitfest/what-is-postgres-commitfest.jpg
|
||||
tags:
|
||||
- postgres
|
||||
- planetpg
|
||||
date: '2022-10-27'
|
||||
toc_depth: 3
|
||||
---
|
||||
|
||||
@@ -6,6 +6,7 @@ image: 2022-11-24-sql-nosql-postgresql/sql-nosql-postgresql.jpg
|
||||
thumb: 2022-11-24-sql-nosql-postgresql/sql-nosql-postgresql.jpg
|
||||
tags:
|
||||
- postgres
|
||||
- planetpg
|
||||
date: '2022-11-24'
|
||||
toc_depth: 3
|
||||
---
|
||||
|
||||
@@ -6,6 +6,7 @@ image: transparent-column-encryption-with-postgres.jpg
|
||||
thumb: transparent-column-encryption-with-postgres.jpg
|
||||
tags:
|
||||
- postgres
|
||||
- planetpg
|
||||
date: '2022-12-01'
|
||||
toc_depth: 3
|
||||
---
|
||||
|
||||
@@ -7,6 +7,7 @@ thumb: lw6-community/pg15.png
|
||||
tags:
|
||||
- postgres
|
||||
- launch-week
|
||||
- planetpg
|
||||
date: '2022-12-16'
|
||||
toc_depth: 3
|
||||
---
|
||||
|
||||
@@ -8,6 +8,7 @@ tags:
|
||||
- graphql
|
||||
- postgres
|
||||
- launch-week
|
||||
- planetpg
|
||||
date: '2022-12-16'
|
||||
toc_depth: 3
|
||||
---
|
||||
|
||||
@@ -8,6 +8,7 @@ tags:
|
||||
- launch-week
|
||||
- postgres
|
||||
- backups
|
||||
- planetpg
|
||||
date: '2022-12-16'
|
||||
toc_depth: 3
|
||||
---
|
||||
|
||||
@@ -7,6 +7,7 @@ thumb: lw6-community/postgrest.png
|
||||
tags:
|
||||
- postgres
|
||||
- launch-week
|
||||
- planetpg
|
||||
date: '2022-12-16'
|
||||
toc_depth: 3
|
||||
---
|
||||
|
||||
@@ -8,6 +8,7 @@ tags:
|
||||
- launch-week
|
||||
- postgres
|
||||
- encryption
|
||||
- planetpg
|
||||
date: '2022-12-16'
|
||||
toc_depth: 3
|
||||
---
|
||||
|
||||
@@ -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",
|
||||
|
||||
+25
-2
@@ -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 `
|
||||
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
|
||||
<channel>
|
||||
<title>'Postgres | Supabase Blog</title>
|
||||
<link>https://supabase.com/blog</link>
|
||||
<description>Latest Postgres news from ${authorInfo?.author} at Supabase</description>
|
||||
<language>en</language>
|
||||
<lastBuildDate>${dayjs(posts[0].date)
|
||||
.utc()
|
||||
.format('ddd, DD MMM YYYY HH:hh:mm ZZ')}</lastBuildDate>
|
||||
<atom:link href="https://supabase.com/planetpg-${authorID}-rss.xml" rel="self" type="application/rss+xml"/>
|
||||
${posts.map(generateRssItem).join('')}
|
||||
</channel>
|
||||
</rss>
|
||||
`
|
||||
} else {
|
||||
return `
|
||||
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
|
||||
<channel>
|
||||
<title>Blog - Supabase</title>
|
||||
@@ -33,4 +55,5 @@ export const generateRss = (posts: any[]): string => {
|
||||
</channel>
|
||||
</rss>
|
||||
`
|
||||
}
|
||||
}
|
||||
@@ -24,6 +24,19 @@ export async function getStaticProps() {
|
||||
// rss feed is added via <Head> component in render return
|
||||
fs.writeFileSync('./public/rss.xml', rss)
|
||||
|
||||
// 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: {
|
||||
blogs: allPostsData,
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
|
||||
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
|
||||
<channel>
|
||||
<title>'Postgres | Supabase Blog</title>
|
||||
<link>https://supabase.com/blog</link>
|
||||
<description>Latest Postgres news from Angelico de los Reyes at Supabase</description>
|
||||
<language>en</language>
|
||||
<lastBuildDate>Thu, 15 Dec 2022 03:03:30 +0000</lastBuildDate>
|
||||
<atom:link href="https://supabase.com/planetpg-angelico_de_los_reyes-rss.xml" rel="self" type="application/rss+xml"/>
|
||||
<item>
|
||||
<guid>https://supabase.com/blog/postgres-point-in-time-recovery</guid>
|
||||
<title>Point in Time Recovery is now available for Pro projects</title>
|
||||
<link>https://supabase.com/blog/postgres-point-in-time-recovery</link>
|
||||
<description>We're making PITR available for more projects, with a new Dashboard UI that makes it simple to use.</description>
|
||||
<pubDate>Thu, 15 Dec 2022 03:03:30 +0000</pubDate>
|
||||
</item>
|
||||
<item>
|
||||
<guid>https://supabase.com/blog/continuous-postgresql-backup-walg</guid>
|
||||
<title>Continuous PostgreSQL Backups using WAL-G</title>
|
||||
<link>https://supabase.com/blog/continuous-postgresql-backup-walg</link>
|
||||
<description>Have you ever wanted to restore your database's state to a particular moment in time? This post explains how, using WAL-G.</description>
|
||||
<pubDate>Sun, 02 Aug 2020 02:02:30 +0000</pubDate>
|
||||
</item>
|
||||
<item>
|
||||
<guid>https://supabase.com/blog/postgresql-templates</guid>
|
||||
<title>What are PostgreSQL Templates?</title>
|
||||
<link>https://supabase.com/blog/postgresql-templates</link>
|
||||
<description>What are PostgreSQL templates and what are they used for?</description>
|
||||
<pubDate>Thu, 09 Jul 2020 02:02:30 +0000</pubDate>
|
||||
</item>
|
||||
<item>
|
||||
<guid>https://supabase.com/blog/postgresql-physical-logical-backups</guid>
|
||||
<title>Physical vs Logical Backups in PostgreSQL</title>
|
||||
<link>https://supabase.com/blog/postgresql-physical-logical-backups</link>
|
||||
<description>What are physical and logical backups in Postgres?</description>
|
||||
<pubDate>Tue, 07 Jul 2020 02:02:30 +0000</pubDate>
|
||||
</item>
|
||||
|
||||
</channel>
|
||||
</rss>
|
||||
@@ -0,0 +1,19 @@
|
||||
|
||||
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
|
||||
<channel>
|
||||
<title>'Postgres | Supabase Blog</title>
|
||||
<link>https://supabase.com/blog</link>
|
||||
<description>Latest Postgres news from Ant Wilson at Supabase</description>
|
||||
<language>en</language>
|
||||
<lastBuildDate>Sat, 27 Feb 2021 03:03:30 +0000</lastBuildDate>
|
||||
<atom:link href="https://supabase.com/planetpg-ant_wilson-rss.xml" rel="self" type="application/rss+xml"/>
|
||||
<item>
|
||||
<guid>https://supabase.com/blog/cracking-postgres-interview</guid>
|
||||
<title>Cracking PostgreSQL Interview Questions</title>
|
||||
<link>https://supabase.com/blog/cracking-postgres-interview</link>
|
||||
<description>Understand the top PostgreSQL Interview Questions</description>
|
||||
<pubDate>Sat, 27 Feb 2021 03:03:30 +0000</pubDate>
|
||||
</item>
|
||||
|
||||
</channel>
|
||||
</rss>
|
||||
@@ -0,0 +1,26 @@
|
||||
|
||||
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
|
||||
<channel>
|
||||
<title>'Postgres | Supabase Blog</title>
|
||||
<link>https://supabase.com/blog</link>
|
||||
<description>Latest Postgres news from Mark Burggraf at Supabase</description>
|
||||
<language>en</language>
|
||||
<lastBuildDate>Wed, 23 Nov 2022 03:03:30 +0000</lastBuildDate>
|
||||
<atom:link href="https://supabase.com/planetpg-burggraf-rss.xml" rel="self" type="application/rss+xml"/>
|
||||
<item>
|
||||
<guid>https://supabase.com/blog/sql-or-nosql-both-with-postgresql</guid>
|
||||
<title>SQL or NoSQL? Why not use both (with PostgreSQL)?</title>
|
||||
<link>https://supabase.com/blog/sql-or-nosql-both-with-postgresql</link>
|
||||
<description>How to turn Postgres into an easy-to-use NoSQL database that retains all the power of SQL</description>
|
||||
<pubDate>Wed, 23 Nov 2022 03:03:30 +0000</pubDate>
|
||||
</item>
|
||||
<item>
|
||||
<guid>https://supabase.com/blog/postgres-wasm</guid>
|
||||
<title>Postgres WASM by Snaplet and Supabase</title>
|
||||
<link>https://supabase.com/blog/postgres-wasm</link>
|
||||
<description>We're open sourcing postgres-wasm, a PostgresQL server that runs inside a browser, with our friends at Snaplet.</description>
|
||||
<pubDate>Sun, 02 Oct 2022 02:02:30 +0000</pubDate>
|
||||
</item>
|
||||
|
||||
</channel>
|
||||
</rss>
|
||||
@@ -0,0 +1,26 @@
|
||||
|
||||
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
|
||||
<channel>
|
||||
<title>'Postgres | Supabase Blog</title>
|
||||
<link>https://supabase.com/blog</link>
|
||||
<description>Latest Postgres news from Michel Pelletier at Supabase</description>
|
||||
<language>en</language>
|
||||
<lastBuildDate>Thu, 15 Dec 2022 03:03:30 +0000</lastBuildDate>
|
||||
<atom:link href="https://supabase.com/planetpg-michel-rss.xml" rel="self" type="application/rss+xml"/>
|
||||
<item>
|
||||
<guid>https://supabase.com/blog/vault-now-in-beta</guid>
|
||||
<title>Supabase Vault is now in Beta</title>
|
||||
<link>https://supabase.com/blog/vault-now-in-beta</link>
|
||||
<description>A Postgres extension to store encrypted secrets and encrypt data.</description>
|
||||
<pubDate>Thu, 15 Dec 2022 03:03:30 +0000</pubDate>
|
||||
</item>
|
||||
<item>
|
||||
<guid>https://supabase.com/blog/transparent-column-encryption-with-postgres</guid>
|
||||
<title>Transparent Column Encryption with Postgres</title>
|
||||
<link>https://supabase.com/blog/transparent-column-encryption-with-postgres</link>
|
||||
<description>Using pgsodium's Transparent Column Encryption to encrypt data and provide your users with row-level encryption.</description>
|
||||
<pubDate>Wed, 30 Nov 2022 03:03:30 +0000</pubDate>
|
||||
</item>
|
||||
|
||||
</channel>
|
||||
</rss>
|
||||
@@ -0,0 +1,26 @@
|
||||
|
||||
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
|
||||
<channel>
|
||||
<title>'Postgres | Supabase Blog</title>
|
||||
<link>https://supabase.com/blog</link>
|
||||
<description>Latest Postgres news from Oliver Rice at Supabase</description>
|
||||
<language>en</language>
|
||||
<lastBuildDate>Thu, 15 Dec 2022 03:03:30 +0000</lastBuildDate>
|
||||
<atom:link href="https://supabase.com/planetpg-oli_rice-rss.xml" rel="self" type="application/rss+xml"/>
|
||||
<item>
|
||||
<guid>https://supabase.com/blog/pg-graphql-v1</guid>
|
||||
<title>pg_graphql v1.0</title>
|
||||
<link>https://supabase.com/blog/pg-graphql-v1</link>
|
||||
<description>Announcing the v1.0 release of pg_graphql</description>
|
||||
<pubDate>Thu, 15 Dec 2022 03:03:30 +0000</pubDate>
|
||||
</item>
|
||||
<item>
|
||||
<guid>https://supabase.com/blog/audit</guid>
|
||||
<title>Postgres Auditing in 150 lines of SQL</title>
|
||||
<link>https://supabase.com/blog/audit</link>
|
||||
<description>PostgreSQL has a robust set of features which we can leverage to create a generic auditing solution in 150 lines of SQL.</description>
|
||||
<pubDate>Mon, 07 Mar 2022 03:03:30 +0000</pubDate>
|
||||
</item>
|
||||
|
||||
</channel>
|
||||
</rss>
|
||||
@@ -0,0 +1,33 @@
|
||||
|
||||
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
|
||||
<channel>
|
||||
<title>'Postgres | Supabase Blog</title>
|
||||
<link>https://supabase.com/blog</link>
|
||||
<description>Latest Postgres news from Paul Copplestone at Supabase</description>
|
||||
<language>en</language>
|
||||
<lastBuildDate>Mon, 27 Jun 2022 02:02:30 +0000</lastBuildDate>
|
||||
<atom:link href="https://supabase.com/planetpg-paul_copplestone-rss.xml" rel="self" type="application/rss+xml"/>
|
||||
<item>
|
||||
<guid>https://supabase.com/blog/partial-postgresql-data-dumps-with-rls</guid>
|
||||
<title>Partial data dumps using Postgres Row Level Security</title>
|
||||
<link>https://supabase.com/blog/partial-postgresql-data-dumps-with-rls</link>
|
||||
<description>Using RLS to create seed files for local PostgreSQL testing.</description>
|
||||
<pubDate>Mon, 27 Jun 2022 02:02:30 +0000</pubDate>
|
||||
</item>
|
||||
<item>
|
||||
<guid>https://supabase.com/blog/postgres-as-a-cron-server</guid>
|
||||
<title>Postgres as a CRON Server</title>
|
||||
<link>https://supabase.com/blog/postgres-as-a-cron-server</link>
|
||||
<description>Running repetitive tasks with your Postgres database.</description>
|
||||
<pubDate>Fri, 05 Mar 2021 03:03:30 +0000</pubDate>
|
||||
</item>
|
||||
<item>
|
||||
<guid>https://supabase.com/blog/postgresql-views</guid>
|
||||
<title>Postgres Views</title>
|
||||
<link>https://supabase.com/blog/postgresql-views</link>
|
||||
<description>Creating and using a view in PostgreSQL.</description>
|
||||
<pubDate>Wed, 18 Nov 2020 03:03:30 +0000</pubDate>
|
||||
</item>
|
||||
|
||||
</channel>
|
||||
</rss>
|
||||
@@ -0,0 +1,26 @@
|
||||
|
||||
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
|
||||
<channel>
|
||||
<title>'Postgres | Supabase Blog</title>
|
||||
<link>https://supabase.com/blog</link>
|
||||
<description>Latest Postgres news from Pavel Borisov at Supabase</description>
|
||||
<language>en</language>
|
||||
<lastBuildDate>Thu, 15 Dec 2022 03:03:30 +0000</lastBuildDate>
|
||||
<atom:link href="https://supabase.com/planetpg-pavel-rss.xml" rel="self" type="application/rss+xml"/>
|
||||
<item>
|
||||
<guid>https://supabase.com/blog/new-in-postgres-15</guid>
|
||||
<title>What's new in Postgres 15?</title>
|
||||
<link>https://supabase.com/blog/new-in-postgres-15</link>
|
||||
<description>Describes the release of Postgres 15, new features and reasons to use it</description>
|
||||
<pubDate>Thu, 15 Dec 2022 03:03:30 +0000</pubDate>
|
||||
</item>
|
||||
<item>
|
||||
<guid>https://supabase.com/blog/postgresql-commitfest</guid>
|
||||
<title>What is PostgreSQL commitfest and how to contribute</title>
|
||||
<link>https://supabase.com/blog/postgresql-commitfest</link>
|
||||
<description>A time-tested method for contributing to the core Postgres code</description>
|
||||
<pubDate>Wed, 26 Oct 2022 02:02:30 +0000</pubDate>
|
||||
</item>
|
||||
|
||||
</channel>
|
||||
</rss>
|
||||
@@ -0,0 +1,26 @@
|
||||
|
||||
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
|
||||
<channel>
|
||||
<title>'Postgres | Supabase Blog</title>
|
||||
<link>https://supabase.com/blog</link>
|
||||
<description>Latest Postgres news from Steve Chavez at Supabase</description>
|
||||
<language>en</language>
|
||||
<lastBuildDate>Thu, 15 Dec 2022 03:03:30 +0000</lastBuildDate>
|
||||
<atom:link href="https://supabase.com/planetpg-steve_chavez-rss.xml" rel="self" type="application/rss+xml"/>
|
||||
<item>
|
||||
<guid>https://supabase.com/blog/postgrest-11-prerelease</guid>
|
||||
<title>PostgREST 11 pre-release</title>
|
||||
<link>https://supabase.com/blog/postgrest-11-prerelease</link>
|
||||
<description>Describes new features of PostgREST 11 pre-release</description>
|
||||
<pubDate>Thu, 15 Dec 2022 03:03:30 +0000</pubDate>
|
||||
</item>
|
||||
<item>
|
||||
<guid>https://supabase.com/blog/roles-postgres-hooks</guid>
|
||||
<title>Protecting reserved roles with PostgreSQL Hooks</title>
|
||||
<link>https://supabase.com/blog/roles-postgres-hooks</link>
|
||||
<description>Using Postgres Hooks to protect functionality in your Postgres database.</description>
|
||||
<pubDate>Thu, 01 Jul 2021 02:02:30 +0000</pubDate>
|
||||
</item>
|
||||
|
||||
</channel>
|
||||
</rss>
|
||||
@@ -0,0 +1,33 @@
|
||||
|
||||
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
|
||||
<channel>
|
||||
<title>'Postgres | Supabase Blog</title>
|
||||
<link>https://supabase.com/blog</link>
|
||||
<description>Latest Postgres news from Victor at Supabase</description>
|
||||
<language>en</language>
|
||||
<lastBuildDate>Thu, 13 Oct 2022 02:02:30 +0000</lastBuildDate>
|
||||
<atom:link href="https://supabase.com/planetpg-victor-rss.xml" rel="self" type="application/rss+xml"/>
|
||||
<item>
|
||||
<guid>https://supabase.com/blog/postgres-full-text-search-vs-the-rest</guid>
|
||||
<title>Postgres Full Text Search vs the rest</title>
|
||||
<link>https://supabase.com/blog/postgres-full-text-search-vs-the-rest</link>
|
||||
<description>Comparing one of the most popular Postgres features against alternatives</description>
|
||||
<pubDate>Thu, 13 Oct 2022 02:02:30 +0000</pubDate>
|
||||
</item>
|
||||
<item>
|
||||
<guid>https://supabase.com/blog/choosing-a-postgres-primary-key</guid>
|
||||
<title>Choosing a Postgres Primary Key</title>
|
||||
<link>https://supabase.com/blog/choosing-a-postgres-primary-key</link>
|
||||
<description>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</description>
|
||||
<pubDate>Wed, 07 Sep 2022 02:02:30 +0000</pubDate>
|
||||
</item>
|
||||
<item>
|
||||
<guid>https://supabase.com/blog/seen-by-in-postgresql</guid>
|
||||
<title>Implementing "seen by" functionality with Postgres</title>
|
||||
<link>https://supabase.com/blog/seen-by-in-postgresql</link>
|
||||
<description>Different approaches for tracking visitor counts with PostgreSQL.</description>
|
||||
<pubDate>Sun, 17 Jul 2022 02:02:30 +0000</pubDate>
|
||||
</item>
|
||||
|
||||
</channel>
|
||||
</rss>
|
||||
+196
-154
File diff suppressed because it is too large.
Load diff
@@ -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
|
||||
|
||||
Generated
+105
-105
@@ -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",
|
||||
|
||||
@@ -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": [
|
||||
{
|
||||
|
||||
@@ -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": [
|
||||
{
|
||||
|
||||
@@ -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": [
|
||||
{
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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<any[]>([])
|
||||
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 (
|
||||
<div className="flex flex-col items-center justify-center w-full h-full space-y-4">
|
||||
<div className="space-y-2 flex flex-col items-center w-[400px]">
|
||||
<p>Unsupported wrapper type</p>
|
||||
<p className="text-sm text-center text-scale-1000">
|
||||
The wrapper type {type} not supported by the dashboard. Head back to create a different
|
||||
wrapper.
|
||||
</p>
|
||||
</div>
|
||||
<Link href={`/project/${ref}/database/wrappers`}>
|
||||
<a>
|
||||
<Button type="default">Head back</Button>
|
||||
</a>
|
||||
</Link>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
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 (
|
||||
<FormsContainer>
|
||||
<div>
|
||||
<div className="relative flex items-center justify-between mb-6">
|
||||
<div
|
||||
className={[
|
||||
'transition cursor-pointer',
|
||||
'absolute -left-20 top-1 opacity-75 hover:opacity-100',
|
||||
].join(' ')}
|
||||
>
|
||||
<Link href={`/project/${ref}/database/wrappers`}>
|
||||
<a>
|
||||
<div className="flex items-center space-x-2">
|
||||
<IconArrowLeft strokeWidth={1.5} size={14} />
|
||||
<p className="text-sm">Back</p>
|
||||
</div>
|
||||
</a>
|
||||
</Link>
|
||||
</div>
|
||||
<h3 className="mb-2 text-xl text-scale-1200">Create a {wrapperMeta?.label} Wrapper</h3>
|
||||
<div className="flex items-center space-x-2">
|
||||
<Link href="https://supabase.github.io/wrappers/stripe/">
|
||||
<a target="_blank">
|
||||
<Button type="default" icon={<IconExternalLink strokeWidth={1.5} />}>
|
||||
Documentation
|
||||
</Button>
|
||||
</a>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Form id={formId} initialValues={initialValues} onSubmit={onSubmit}>
|
||||
{({ isSubmitting, handleReset, values, initialValues }: any) => {
|
||||
const hasChanges = JSON.stringify(values) !== JSON.stringify(initialValues)
|
||||
return (
|
||||
<FormPanel
|
||||
disabled={!canCreateWrapper}
|
||||
footer={
|
||||
<div className="flex px-8 py-4">
|
||||
<FormActions
|
||||
form={formId}
|
||||
isSubmitting={isSubmitting}
|
||||
hasChanges={hasChanges}
|
||||
handleReset={handleReset}
|
||||
helper={
|
||||
!canCreateWrapper
|
||||
? 'You need additional permissions to create a foreign data wrapper'
|
||||
: undefined
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<FormSection header={<FormSectionLabel>Wrapper Configuration</FormSectionLabel>}>
|
||||
<FormSectionContent loading={false}>
|
||||
<Input
|
||||
id="wrapper_name"
|
||||
label="Wrapper Name"
|
||||
error={formErrors.wrapper_name}
|
||||
descriptionText={
|
||||
(values?.wrapper_name ?? '').length > 0 ? (
|
||||
<>
|
||||
Your wrapper's server name will be{' '}
|
||||
<code className="text-xs">{values.wrapper_name}_server</code>
|
||||
</>
|
||||
) : (
|
||||
''
|
||||
)
|
||||
}
|
||||
/>
|
||||
</FormSectionContent>
|
||||
</FormSection>
|
||||
<FormSection
|
||||
header={<FormSectionLabel>{wrapperMeta.label} Configuration</FormSectionLabel>}
|
||||
>
|
||||
<FormSectionContent loading={false}>
|
||||
{wrapperMeta.server.options.map((option) => (
|
||||
<InputField
|
||||
key={option.name}
|
||||
option={option}
|
||||
loading={false}
|
||||
error={formErrors[option.name]}
|
||||
/>
|
||||
))}
|
||||
</FormSectionContent>
|
||||
</FormSection>
|
||||
<FormSection
|
||||
header={
|
||||
<FormSectionLabel>
|
||||
<p>Foreign Tables</p>
|
||||
<p className="text-scale-1000 mt-2 w-[90%]">
|
||||
You can query your data from these foreign tables after the wrapper is
|
||||
created
|
||||
</p>
|
||||
</FormSectionLabel>
|
||||
}
|
||||
>
|
||||
<FormSectionContent loading={false}>
|
||||
{newTables.length === 0 ? (
|
||||
<div className="flex justify-end translate-y-4">
|
||||
<Button type="default" onClick={() => setIsEditingTable(true)}>
|
||||
Add foreign table
|
||||
</Button>
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-2">
|
||||
{newTables.map((table, i) => (
|
||||
<div className="flex items-center justify-between px-4 py-2 border rounded-md border-scale-600">
|
||||
<div>
|
||||
<p className="text-sm">
|
||||
{table.schema_name}.{table.table_name}
|
||||
</p>
|
||||
<p className="text-sm text-scale-1000">
|
||||
{wrapperMeta.tables[table.index].label}: {table.columns.join(', ')}
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex items-center space-x-2">
|
||||
<Button
|
||||
type="default"
|
||||
className="px-1"
|
||||
icon={<IconEdit />}
|
||||
onClick={() => {
|
||||
setIsEditingTable(true)
|
||||
setSelectedTableToEdit({ ...table, tableIndex: i })
|
||||
}}
|
||||
/>
|
||||
<Button
|
||||
type="default"
|
||||
className="px-1"
|
||||
icon={<IconTrash />}
|
||||
onClick={() => {
|
||||
setNewTables((prev) => prev.filter((_, j) => j !== i))
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
{newTables.length > 0 && (
|
||||
<div className="flex justify-end">
|
||||
<Button type="default" onClick={() => setIsEditingTable(true)}>
|
||||
Add foreign table
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
{newTables.length === 0 && formErrors.tables && (
|
||||
<p className="text-sm text-right text-red-900">{formErrors.tables}</p>
|
||||
)}
|
||||
</FormSectionContent>
|
||||
</FormSection>
|
||||
</FormPanel>
|
||||
)
|
||||
}}
|
||||
</Form>
|
||||
</div>
|
||||
|
||||
<WrapperTableEditor
|
||||
visible={isEditingTable}
|
||||
tables={wrapperMeta.tables}
|
||||
onCancel={() => {
|
||||
setSelectedTableToEdit(undefined)
|
||||
setIsEditingTable(false)
|
||||
}}
|
||||
onSave={onUpdateTable}
|
||||
initialData={selectedTableToEdit}
|
||||
/>
|
||||
</FormsContainer>
|
||||
)
|
||||
}
|
||||
|
||||
export default observer(CreateWrapper)
|
||||
@@ -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<any[]>([])
|
||||
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 (
|
||||
<div className="flex items-center justify-center w-full h-full">
|
||||
<Loading />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
if (wrapper === undefined || wrapperMeta === undefined) {
|
||||
if (isSaving) {
|
||||
return (
|
||||
<div className="flex flex-col items-center justify-center w-full h-full space-y-4">
|
||||
<div className="flex items-center space-x-4">
|
||||
<IconLoader className="animate-spin" size={16} />
|
||||
<p className="text-sm">Updating wrapper</p>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
} else {
|
||||
return (
|
||||
<div className="flex flex-col items-center justify-center w-full h-full space-y-4">
|
||||
<div className="space-y-2 flex flex-col items-center w-[400px]">
|
||||
<p>Unknown wrapper</p>
|
||||
<p className="text-sm text-center text-scale-1000">
|
||||
The wrapper ID {id} cannot be found in your project. Head back to select another
|
||||
wrapper.
|
||||
</p>
|
||||
</div>
|
||||
<Link href={`/project/${ref}/database/wrappers`}>
|
||||
<a>
|
||||
<Button type="default">Head back</Button>
|
||||
</a>
|
||||
</Link>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
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 (
|
||||
<FormsContainer>
|
||||
<div>
|
||||
<div className="relative flex items-center justify-between mb-6">
|
||||
<div
|
||||
className={[
|
||||
'transition cursor-pointer',
|
||||
'absolute -left-20 top-1 opacity-75 hover:opacity-100',
|
||||
].join(' ')}
|
||||
>
|
||||
<Link href={`/project/${ref}/database/wrappers`}>
|
||||
<a>
|
||||
<div className="flex items-center space-x-2">
|
||||
<IconArrowLeft strokeWidth={1.5} size={14} />
|
||||
<p className="text-sm">Back</p>
|
||||
</div>
|
||||
</a>
|
||||
</Link>
|
||||
</div>
|
||||
<h3 className="mb-2 text-xl text-scale-1200">Edit wrapper: {wrapper.name}</h3>
|
||||
<div className="flex items-center space-x-2">
|
||||
<Link href="https://supabase.github.io/wrappers/stripe/">
|
||||
<a target="_blank">
|
||||
<Button type="default" icon={<IconExternalLink strokeWidth={1.5} />}>
|
||||
Documentation
|
||||
</Button>
|
||||
</a>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Form id={formId} initialValues={initialValues} onSubmit={onSubmit}>
|
||||
{({ 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 (
|
||||
<FormPanel
|
||||
disabled={!canCreateWrapper}
|
||||
footer={
|
||||
<div className="flex px-8 py-4">
|
||||
<FormActions
|
||||
form={formId}
|
||||
isSubmitting={isSubmitting}
|
||||
hasChanges={hasChanges}
|
||||
handleReset={() => {
|
||||
handleReset()
|
||||
setWrapperTables(initialTables)
|
||||
}}
|
||||
helper={
|
||||
!canCreateWrapper
|
||||
? 'You need additional permissions to create a foreign data wrapper'
|
||||
: undefined
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<FormSection header={<FormSectionLabel>Wrapper Configuration</FormSectionLabel>}>
|
||||
<FormSectionContent loading={false}>
|
||||
<Input
|
||||
id="wrapper_name"
|
||||
label="Wrapper Name"
|
||||
error={formErrors.wrapper_name}
|
||||
descriptionText={
|
||||
values.wrapper_name !== initialValues.wrapper_name ? (
|
||||
<>
|
||||
Your wrapper's server name will be updated to{' '}
|
||||
<code className="text-xs">{values.wrapper_name}_server</code>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
Your wrapper's server name is{' '}
|
||||
<code className="text-xs">{values.wrapper_name}_server</code>
|
||||
</>
|
||||
)
|
||||
}
|
||||
/>
|
||||
</FormSectionContent>
|
||||
</FormSection>
|
||||
<FormSection
|
||||
header={<FormSectionLabel>{wrapperMeta.label} Configuration</FormSectionLabel>}
|
||||
>
|
||||
<FormSectionContent loading={false}>
|
||||
{wrapperMeta.server.options.map((option) => (
|
||||
<InputField
|
||||
key={option.name}
|
||||
option={option}
|
||||
loading={option.encrypted ? loadingSecrets : false}
|
||||
error={formErrors[option.name]}
|
||||
/>
|
||||
))}
|
||||
</FormSectionContent>
|
||||
</FormSection>
|
||||
<FormSection
|
||||
header={
|
||||
<FormSectionLabel>
|
||||
<p>Foreign Tables</p>
|
||||
<p className="text-scale-1000 mt-2 w-[90%]">
|
||||
You can query your data from these foreign tables after the wrapper is
|
||||
created
|
||||
</p>
|
||||
</FormSectionLabel>
|
||||
}
|
||||
>
|
||||
<FormSectionContent loading={false}>
|
||||
{wrapperTables.length === 0 ? (
|
||||
<div className="flex justify-end translate-y-4">
|
||||
<Button type="default" onClick={() => setIsEditingTable(true)}>
|
||||
Add foreign table
|
||||
</Button>
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-2">
|
||||
{wrapperTables.map((table, i) => (
|
||||
<div className="flex items-center justify-between px-4 py-2 border rounded-md border-scale-600">
|
||||
<div>
|
||||
<p className="text-sm">
|
||||
{table.schema_name}.{table.table_name}
|
||||
</p>
|
||||
<p className="text-sm text-scale-1000">
|
||||
{wrapperMeta.tables[table.index].label}: {table.columns.join(', ')}
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex items-center space-x-2">
|
||||
<Button
|
||||
type="default"
|
||||
className="px-1"
|
||||
icon={<IconEdit />}
|
||||
onClick={() => {
|
||||
setIsEditingTable(true)
|
||||
setSelectedTableToEdit({ ...table, tableIndex: i })
|
||||
}}
|
||||
/>
|
||||
<Button
|
||||
type="default"
|
||||
className="px-1"
|
||||
icon={<IconTrash />}
|
||||
onClick={() => {
|
||||
setWrapperTables((prev) => prev.filter((_, j) => j !== i))
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
{wrapperTables.length > 0 && (
|
||||
<div className="flex justify-end">
|
||||
<Button type="default" onClick={() => setIsEditingTable(true)}>
|
||||
Add foreign table
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
{wrapperTables.length === 0 && formErrors.tables && (
|
||||
<p className="text-sm text-right text-red-900">{formErrors.tables}</p>
|
||||
)}
|
||||
</FormSectionContent>
|
||||
</FormSection>
|
||||
</FormPanel>
|
||||
)
|
||||
}}
|
||||
</Form>
|
||||
</div>
|
||||
|
||||
<WrapperTableEditor
|
||||
visible={isEditingTable}
|
||||
tables={wrapperMeta.tables}
|
||||
onCancel={() => {
|
||||
setSelectedTableToEdit(undefined)
|
||||
setIsEditingTable(false)
|
||||
}}
|
||||
onSave={onUpdateTable}
|
||||
initialData={selectedTableToEdit}
|
||||
/>
|
||||
</FormsContainer>
|
||||
)
|
||||
}
|
||||
|
||||
export default observer(EditWrapper)
|
||||
@@ -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<Props> = ({ option, loading, error }) => {
|
||||
const [showHidden, setShowHidden] = useState(!option.hidden)
|
||||
if (option.isTextArea) {
|
||||
return (
|
||||
<div className="text-area-text-sm text-area-resize-none">
|
||||
<Input.TextArea
|
||||
key={option.name}
|
||||
rows={6}
|
||||
error={error}
|
||||
className="input-mono"
|
||||
disabled={loading}
|
||||
id={option.name}
|
||||
name={option.name}
|
||||
label={
|
||||
<div className="flex items-center space-x-2">
|
||||
<p>{option.label}</p>
|
||||
{option.urlHelper !== undefined && (
|
||||
<Link href={option.urlHelper}>
|
||||
<a target="_blank">
|
||||
<IconHelpCircle
|
||||
strokeWidth={2}
|
||||
size={14}
|
||||
className="text-scale-1000 hover:text-scale-1200 cursor-pointer transition"
|
||||
/>
|
||||
</a>
|
||||
</Link>
|
||||
)}
|
||||
</div>
|
||||
}
|
||||
value={loading ? 'Fetching value from Vault...' : undefined}
|
||||
defaultValue={option.defaultValue ?? ''}
|
||||
required={option.required ?? false}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
} else {
|
||||
return (
|
||||
<Input
|
||||
key={option.name}
|
||||
id={option.name}
|
||||
name={option.name}
|
||||
label={
|
||||
<div className="flex items-center space-x-2">
|
||||
<p>{option.label}</p>
|
||||
{option.urlHelper !== undefined && (
|
||||
<Link href={option.urlHelper}>
|
||||
<a target="_blank">
|
||||
<IconHelpCircle
|
||||
strokeWidth={2}
|
||||
size={14}
|
||||
className="text-scale-1000 hover:text-scale-1200 cursor-pointer transition"
|
||||
/>
|
||||
</a>
|
||||
</Link>
|
||||
)}
|
||||
</div>
|
||||
}
|
||||
defaultValue={option.defaultValue ?? ''}
|
||||
error={error}
|
||||
value={loading ? 'Fetching value from Vault...' : undefined}
|
||||
type={!option.hidden || loading ? 'text' : showHidden ? 'text' : 'password'}
|
||||
disabled={loading}
|
||||
actions={
|
||||
loading ? (
|
||||
<div className="flex items-center justify-center mr-1">
|
||||
<Button disabled type="default" icon={<IconLoader className="animate-spin" />} />
|
||||
</div>
|
||||
) : option.hidden ? (
|
||||
<div className="flex items-center justify-center mr-1">
|
||||
<Button
|
||||
type="default"
|
||||
icon={showHidden ? <IconEye /> : <IconEyeOff />}
|
||||
onClick={() => setShowHidden(!showHidden)}
|
||||
/>
|
||||
</div>
|
||||
) : null
|
||||
}
|
||||
/>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export default InputField
|
||||
@@ -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 (
|
||||
<div className="text-area-text-sm text-area-resize-none">
|
||||
<Input.TextArea
|
||||
key={option.name}
|
||||
id={option.name}
|
||||
name={option.name}
|
||||
label={
|
||||
<div className="flex items-center space-x-2">
|
||||
<p>{option.label}</p>
|
||||
{option.urlHelper !== undefined && (
|
||||
<Link href={option.urlHelper}>
|
||||
<a target="_blank">
|
||||
<IconHelpCircle
|
||||
strokeWidth={2}
|
||||
size={14}
|
||||
className="text-scale-1000 hover:text-scale-1200 cursor-pointer transition"
|
||||
/>
|
||||
</a>
|
||||
</Link>
|
||||
)}
|
||||
</div>
|
||||
}
|
||||
defaultValue={option.defaultValue ?? ''}
|
||||
required={option.required ?? false}
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
error={error}
|
||||
className="input-mono"
|
||||
rows={6}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
} else {
|
||||
return (
|
||||
<Input
|
||||
key={option.name}
|
||||
id={option.name}
|
||||
name={option.name}
|
||||
label={
|
||||
<div className="flex items-center space-x-2">
|
||||
<p>{option.label}</p>
|
||||
{option.urlHelper !== undefined && (
|
||||
<Link href={option.urlHelper}>
|
||||
<a target="_blank">
|
||||
<IconHelpCircle
|
||||
strokeWidth={2}
|
||||
size={14}
|
||||
className="text-scale-1000 hover:text-scale-1200 cursor-pointer transition"
|
||||
/>
|
||||
</a>
|
||||
</Link>
|
||||
)}
|
||||
</div>
|
||||
}
|
||||
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 ? (
|
||||
<div className="flex items-center justify-center mr-1">
|
||||
<Button
|
||||
type="default"
|
||||
icon={showHidden ? <IconEye /> : <IconEyeOff />}
|
||||
onClick={() => setShowHidden(!showHidden)}
|
||||
/>
|
||||
</div>
|
||||
) : null
|
||||
}
|
||||
/>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
const WrapperRow: FC<Props> = ({ wrapper, tables, isLoading, isEnabled, isOpen, onOpen }) => {
|
||||
const getInitialFormState = () =>
|
||||
Object.fromEntries(
|
||||
wrapper.server.options.map((option) => [option.name, option.defaultValue ?? ''])
|
||||
)
|
||||
|
||||
const WrapperRow: FC<Props> = ({ 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<any[]>([])
|
||||
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<Props> = ({ wrapper, tables, isLoading, isEnabled, isOpen,
|
||||
<>
|
||||
<Collapsible
|
||||
open={isOpen}
|
||||
onOpenChange={() => 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<Props> = ({ 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(' ')}
|
||||
>
|
||||
<Collapsible.Trigger asChild>
|
||||
@@ -254,209 +82,102 @@ const WrapperRow: FC<Props> = ({ wrapper, tables, isLoading, isEnabled, isOpen,
|
||||
width={14}
|
||||
/>
|
||||
<Image
|
||||
src={wrapper.icon}
|
||||
src={wrapperMeta.icon}
|
||||
width={20}
|
||||
height={20}
|
||||
alt={`${wrapper.name} wrapper icon`}
|
||||
alt={`${wrapperMeta.name} wrapper icon`}
|
||||
/>
|
||||
<span className="text-sm capitalize">{wrapper.label}</span>
|
||||
<span className="text-sm capitalize">{wrapperMeta.label}</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-3">
|
||||
{isLoading ? (
|
||||
<div className="flex items-center gap-1 px-1 py-1 text-xs rounded-full">
|
||||
<IconLoader className="animate-spin" size={18} />
|
||||
</div>
|
||||
) : isEnabled ? (
|
||||
<div className="flex items-center gap-1 px-1 py-1 text-xs border rounded-full border-brand-700 bg-brand-200 text-brand-900">
|
||||
<span className="rounded-full bg-brand-900 p-0.5 text-xs text-brand-200">
|
||||
<IconCheck strokeWidth={2} size={12} />
|
||||
</span>
|
||||
<span className="px-1">Enabled</span>
|
||||
</div>
|
||||
) : (
|
||||
<div className="px-3 py-1 text-xs border rounded-md border-scale-500 bg-scale-100 text-scale-900 dark:border-scale-700 dark:bg-scale-300">
|
||||
Disabled
|
||||
</div>
|
||||
)}
|
||||
<div className="px-3 py-1 text-xs border rounded-md border-scale-500 bg-scale-100 text-scale-1200 dark:border-scale-700 dark:bg-scale-300">
|
||||
{wrappers.length} wrapper{wrappers.length > 1 ? 's' : ''}
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
</Collapsible.Trigger>
|
||||
{!isLoading && (
|
||||
<>
|
||||
{isEnabled ? (
|
||||
<Collapsible.Content>
|
||||
<div className="px-6 py-6 border-t group border-scale-500 bg-scale-100 text-scale-1200 dark:bg-scale-300">
|
||||
<div className="max-w-lg mx-auto my-6 space-y-6">
|
||||
<InformationBox
|
||||
hideCollapse
|
||||
defaultVisibility
|
||||
icon={
|
||||
<IconCheckCircle strokeWidth={1.5} size={18} className="text-brand-900" />
|
||||
}
|
||||
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."
|
||||
/>
|
||||
<div className="space-y-2">
|
||||
<p className="text-sm">Foreign Tables</p>
|
||||
<div className="space-y-1">
|
||||
{tables.map((table) => (
|
||||
<div
|
||||
key={table.id}
|
||||
className="rounded border border-scale-600 bg-scale-100 py-3 dark:border-scale-500 dark:bg-scale-400 px-4 py-2 flex items-center justify-between space-x-4"
|
||||
>
|
||||
<div className="space-y-1">
|
||||
<p className="text-sm">
|
||||
{table.schema}.{table.name}
|
||||
</p>
|
||||
<p className="text-sm text-scale-1100">
|
||||
Columns: {table.columns.map((c: any) => c.name).join(', ')}
|
||||
</p>
|
||||
</div>
|
||||
<Link href={`/project/${ref}/editor/${table.id}`}>
|
||||
<a>
|
||||
<Button type="default">View table</Button>
|
||||
</a>
|
||||
</Link>
|
||||
</div>
|
||||
<Collapsible.Content>
|
||||
<div className="border-t group border-scale-500 bg-scale-100 text-scale-1200 dark:bg-scale-300 divide-y">
|
||||
{wrappers.map((wrapper) => {
|
||||
const serverOptions = Object.fromEntries(
|
||||
wrapper.server_options.map((option: any) => option.split('='))
|
||||
)
|
||||
const [encryptedMetadata, visibleMetadata] = partition(
|
||||
wrapperMeta.server.options,
|
||||
'hidden'
|
||||
)
|
||||
|
||||
return (
|
||||
<div key={wrapper.id} className="px-6 py-4 flex items-center justify-between">
|
||||
<div className="space-y-1 w-3/4">
|
||||
<div className="flex items-center space-x-2">
|
||||
<p className="text-base">{wrapper.name}</p>
|
||||
</div>
|
||||
{visibleMetadata.map((metadata) => (
|
||||
<div
|
||||
key={metadata.name}
|
||||
className="flex items-center space-x-2 text-sm text-scale-1000"
|
||||
>
|
||||
<p>{metadata.label}:</p>
|
||||
<p>{serverOptions[metadata.name]}</p>
|
||||
</div>
|
||||
))}
|
||||
{encryptedMetadata.map((metadata) => (
|
||||
<div key={metadata.name} className="flex items-center space-x-2 text-sm">
|
||||
<p className="text-scale-1000">{metadata.label}:</p>
|
||||
<Link
|
||||
href={`/project/${ref}/settings/vault/secrets?search=${wrapper.name}_${metadata.name}`}
|
||||
>
|
||||
<a className="transition text-scale-1000 hover:text-scale-1100 flex items-center space-x-2">
|
||||
<span>Encrypted in Vault</span>
|
||||
<IconExternalLink size={14} strokeWidth={1.5} />
|
||||
</a>
|
||||
</Link>
|
||||
</div>
|
||||
))}
|
||||
<div className="!mt-3 space-y-1">
|
||||
<p className="text-sm text-scale-1100">
|
||||
Foreign tables: ({wrapper.tables.length})
|
||||
</p>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{wrapper.tables.map((table: any) => (
|
||||
<Link href={`/project/${ref}/editor/${table.id}`}>
|
||||
<a>
|
||||
<div
|
||||
key={table.id}
|
||||
className="text-sm border rounded px-2 py-1 transition bg-scale-400 hover:bg-scale-500"
|
||||
>
|
||||
{table.name}
|
||||
</div>
|
||||
</a>
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center justify-end !mt-8">
|
||||
<div className="flex items-center space-x-3">
|
||||
<Link href={wrapper.docsUrl}>
|
||||
<a target="_blank">
|
||||
<Button type="default" icon={<IconExternalLink />}>
|
||||
Documentation
|
||||
</Button>
|
||||
</a>
|
||||
</Link>
|
||||
</div>
|
||||
<div className="flex items-center space-x-2">
|
||||
<Link href={`/project/${ref}/database/wrappers/${wrapper.id}`}>
|
||||
<a>
|
||||
<Button
|
||||
type="default"
|
||||
loading={isSubmitting}
|
||||
onClick={() => onDeleteWrapper()}
|
||||
>
|
||||
Disable wrapper
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
icon={<IconEdit strokeWidth={1.5} />}
|
||||
className="py-2"
|
||||
/>
|
||||
</a>
|
||||
</Link>
|
||||
<Button
|
||||
type="default"
|
||||
icon={<IconTrash strokeWidth={1.5} />}
|
||||
className="py-2"
|
||||
onClick={() => onDeleteWrapper(wrapper)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</Collapsible.Content>
|
||||
) : (
|
||||
<Collapsible.Content>
|
||||
<div className="px-6 py-6 border-t group border-scale-500 bg-scale-100 text-scale-1200 dark:bg-scale-300">
|
||||
<div className="max-w-lg mx-auto my-6 space-y-6">
|
||||
{wrapper.server.options.map((option) => (
|
||||
<InputField
|
||||
option={option}
|
||||
value={formState[option.name]}
|
||||
error={formErrors[option.name]}
|
||||
onChange={(e: any) => {
|
||||
setFormState((prev) => ({ ...prev, [option.name]: e.target.value }))
|
||||
setFormErrors((prev) => ({ ...prev, [option.name]: '' }))
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
<div className="space-y-2">
|
||||
<div className="space-y-1">
|
||||
<div className="flex items-center justify-between">
|
||||
<p className="text-sm text-scale-1100">Foreign Tables</p>
|
||||
<Button type="default" onClick={() => setIsEditingTable(true)}>
|
||||
Add table
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
{newTables.length === 0 && (
|
||||
<div className="flex items-center justify-center px-4 py-4 border rounded-md border-scale-600">
|
||||
<p className="text-sm text-scale-1000">
|
||||
Add foreign tables to query from after the wrapper is enabled
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
{newTables.map((table, i) => (
|
||||
<div className="flex items-center justify-between px-4 py-2 border rounded-md border-scale-600">
|
||||
<div>
|
||||
<p className="text-sm">
|
||||
{table.schema_name}.{table.table_name}
|
||||
</p>
|
||||
<p className="text-sm text-scale-1000">
|
||||
{wrapper.tables[table.index].label}: {table.columns.join(', ')}
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex items-center space-x-2">
|
||||
<Button
|
||||
type="default"
|
||||
className="px-1"
|
||||
icon={<IconEdit />}
|
||||
onClick={() => {
|
||||
setIsEditingTable(true)
|
||||
setSelectedTableToEdit({ ...table, tableIndex: i })
|
||||
}}
|
||||
/>
|
||||
<Button
|
||||
type="default"
|
||||
className="px-1"
|
||||
icon={<IconTrash />}
|
||||
onClick={() => {
|
||||
setNewTables((prev) => prev.filter((_, j) => j !== i))
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
{newTables.length === 0 && formErrors.tables && (
|
||||
<p className="text-sm text-red-900">{formErrors.tables}</p>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex items-center justify-between !mt-8">
|
||||
<Link href={wrapper.docsUrl}>
|
||||
<a target="_blank">
|
||||
<Button type="default" icon={<IconExternalLink />}>
|
||||
Documentation
|
||||
</Button>
|
||||
</a>
|
||||
</Link>
|
||||
<div className="flex items-center space-x-3">
|
||||
<Button
|
||||
type="default"
|
||||
htmlType="reset"
|
||||
onClick={() => {
|
||||
onOpen('')
|
||||
setNewTables([])
|
||||
setFormState(getInitialFormState)
|
||||
}}
|
||||
disabled={isSubmitting}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
loading={isSubmitting}
|
||||
disabled={noChanges}
|
||||
onClick={() => onSaveWrapper()}
|
||||
>
|
||||
Save
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Collapsible.Content>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</Collapsible.Content>
|
||||
</Collapsible>
|
||||
<WrapperTableEditor
|
||||
visible={isEditingTable}
|
||||
tables={wrapper.tables}
|
||||
onCancel={() => {
|
||||
setSelectedTableToEdit(undefined)
|
||||
setIsEditingTable(false)
|
||||
}}
|
||||
onSave={onUpdateTable}
|
||||
initialData={selectedTableToEdit}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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<string>('')
|
||||
const [isEnabling, setIsEnabling] = useState<boolean>(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 (
|
||||
<div>
|
||||
<div className="flex items-center justify-between mb-6">
|
||||
@@ -88,159 +44,63 @@ const Wrappers = () => {
|
||||
Query your data warehouse directly from your database, or third-party APIs using SQL.
|
||||
</div>
|
||||
</div>
|
||||
{/* [Joshen TODO] For when we support multiple instances per wrapper */}
|
||||
{/* <div>
|
||||
<Dropdown
|
||||
side="bottom"
|
||||
align="end"
|
||||
size="small"
|
||||
overlay={
|
||||
<>
|
||||
{WRAPPERS.map((wrapper, idx) => (
|
||||
<>
|
||||
<Dropdown.Item
|
||||
key={wrapper.name}
|
||||
icon={
|
||||
<Image
|
||||
src={wrapper.icon}
|
||||
width={20}
|
||||
height={20}
|
||||
alt={`${wrapper.name} wrapper icon`}
|
||||
/>
|
||||
}
|
||||
onClick={() => {}}
|
||||
>
|
||||
{wrapper.label}
|
||||
</Dropdown.Item>
|
||||
{idx !== WRAPPERS.length - 1 && <Dropdown.Separator />}
|
||||
</>
|
||||
))}
|
||||
</>
|
||||
}
|
||||
>
|
||||
<Button type="primary">Add wrapper</Button>
|
||||
</Dropdown>
|
||||
</div> */}
|
||||
<div className="flex items-center space-x-2">
|
||||
<Link href="">
|
||||
<a target="_blank">
|
||||
<Button type="default" icon={<IconExternalLink strokeWidth={1.5} />}>
|
||||
Documentation
|
||||
</Button>
|
||||
</a>
|
||||
</Link>
|
||||
{isWrappersEnabled && <WrappersDropdown />}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{isLoadingExtensions ? (
|
||||
<div className="border rounded border-scale-500 p-12 space-y-2">
|
||||
{isLoadingExtensions || isLoading ? (
|
||||
<div className="p-12 space-y-2 border rounded border-scale-500">
|
||||
<ShimmeringLoader />
|
||||
<ShimmeringLoader className="w-3/4" />
|
||||
<ShimmeringLoader className="w-1/2" />
|
||||
</div>
|
||||
) : isWrappersEnabled ? (
|
||||
<div>
|
||||
{WRAPPERS.map((wrapper, i) => {
|
||||
return (
|
||||
<WrapperRow
|
||||
key={i}
|
||||
wrapper={wrapper}
|
||||
tables={wrappers.find((w) => 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 ? (
|
||||
<div
|
||||
className={[
|
||||
'border rounded border-scale-500 px-20 py-16',
|
||||
'flex flex-col items-center justify-center space-y-4',
|
||||
].join(' ')}
|
||||
>
|
||||
<p className="text-sm">No wrappers created yet</p>
|
||||
<WrappersDropdown align="center" buttonText="Create a new wrapper" />
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
{/* [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 (
|
||||
<WrapperRow
|
||||
key={i}
|
||||
wrapperMeta={wrapper}
|
||||
wrappers={createdWrappers}
|
||||
isOpen={open === wrapper.name}
|
||||
onOpen={(wrapperName) => {
|
||||
if (open !== wrapperName) setOpen(wrapperName)
|
||||
else setOpen('')
|
||||
}}
|
||||
/>
|
||||
)
|
||||
} else {
|
||||
return null
|
||||
}
|
||||
})}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<div>
|
||||
<div
|
||||
className="w-full px-12 py-12 bg-white bg-no-repeat border rounded dark:bg-scale-200 border-scale-500"
|
||||
style={{
|
||||
backgroundSize: '45%',
|
||||
backgroundPosition: '105% 40%',
|
||||
backgroundImage: ui.isDarkTheme
|
||||
? 'url("/img/wrappers-dark.png")'
|
||||
: 'url("/img/wrappers-light.png")',
|
||||
}}
|
||||
>
|
||||
<div className="w-3/5 space-y-8">
|
||||
<div className="space-y-2">
|
||||
<h4 className="text-lg">Supabase Wrappers</h4>
|
||||
<p className="text-sm text-scale-1100">
|
||||
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.
|
||||
</p>
|
||||
</div>
|
||||
{isNotAvailable ? (
|
||||
<div className="space-y-4">
|
||||
<div className="rounded border border-scale-500 px-4 py-2 flex items-center justify-between">
|
||||
<div>
|
||||
<p className="text-scale-1100 text-sm">
|
||||
Wrappers is not available for this project yet.
|
||||
</p>
|
||||
<p className="text-scale-1000 text-sm">
|
||||
Do reach out to us if you're interested!
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<Link
|
||||
href={`/support/new?ref=${ref}&category=sales&subject=Request%20for%20access%20to%20wrappers`}
|
||||
>
|
||||
<a target="_blank">
|
||||
<Button type="primary">Contact us</Button>
|
||||
</a>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center space-x-2 my-1 ml-[1px]">
|
||||
<Link href="https://supabase.com/docs/guides/database/wrappers">
|
||||
<a target="_blank">
|
||||
<Button type="default" icon={<IconExternalLink />}>
|
||||
About Wrappers
|
||||
</Button>
|
||||
</a>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex items-center space-x-2">
|
||||
<Link href="https://supabase.com/docs/guides/database/wrappers">
|
||||
<a target="_blank">
|
||||
<Button type="default" icon={<IconExternalLink />}>
|
||||
About Wrappers
|
||||
</Button>
|
||||
</a>
|
||||
</Link>
|
||||
<Tooltip.Root delayDuration={0}>
|
||||
<Tooltip.Trigger>
|
||||
<Button
|
||||
type="primary"
|
||||
loading={isEnabling}
|
||||
disabled={isNotAvailable || isEnabling || !canToggleWrappers}
|
||||
onClick={() => onEnableWrappers()}
|
||||
>
|
||||
Enable Wrappers
|
||||
</Button>
|
||||
</Tooltip.Trigger>
|
||||
{!canToggleWrappers && (
|
||||
<Tooltip.Content side="bottom">
|
||||
<Tooltip.Arrow className="radix-tooltip-arrow" />
|
||||
<div
|
||||
className={[
|
||||
'rounded bg-scale-100 py-1 px-2 leading-none shadow',
|
||||
'border border-scale-200',
|
||||
].join(' ')}
|
||||
>
|
||||
<span className="text-xs text-scale-1200">
|
||||
You need additional permissions to enable Wrappers for this project
|
||||
</span>
|
||||
</div>
|
||||
</Tooltip.Content>
|
||||
)}
|
||||
</Tooltip.Root>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<WrappersDisabledState />
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -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[]
|
||||
}
|
||||
|
||||
|
||||
@@ -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('=')))
|
||||
}
|
||||
@@ -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<boolean>(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 (
|
||||
<div>
|
||||
<div
|
||||
className="w-full px-12 py-12 bg-white bg-no-repeat border rounded dark:bg-scale-200 border-scale-500"
|
||||
style={{
|
||||
backgroundSize: '45%',
|
||||
backgroundPosition: '105% 40%',
|
||||
backgroundImage: ui.isDarkTheme
|
||||
? 'url("/img/wrappers-dark.png")'
|
||||
: 'url("/img/wrappers-light.png")',
|
||||
}}
|
||||
>
|
||||
<div className="w-3/5 space-y-8">
|
||||
<div className="space-y-2">
|
||||
<h4 className="text-lg">Supabase Wrappers</h4>
|
||||
<p className="text-sm text-scale-1100">
|
||||
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.
|
||||
</p>
|
||||
</div>
|
||||
{isNotAvailable ? (
|
||||
<div className="space-y-4">
|
||||
<div className="rounded border border-scale-500 px-4 py-2 flex items-center justify-between">
|
||||
<div>
|
||||
<p className="text-scale-1100 text-sm">
|
||||
Wrappers is not available for this project yet.
|
||||
</p>
|
||||
<p className="text-scale-1000 text-sm">
|
||||
Do reach out to us if you're interested!
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<Link
|
||||
href={`/support/new?ref=${ref}&category=sales&subject=Request%20for%20access%20to%20wrappers`}
|
||||
>
|
||||
<a target="_blank">
|
||||
<Button type="primary">Contact us</Button>
|
||||
</a>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center space-x-2 my-1 ml-[1px]">
|
||||
<Link href="https://supabase.com/docs/guides/database/wrappers">
|
||||
<a target="_blank">
|
||||
<Button type="default" icon={<IconExternalLink />}>
|
||||
About Wrappers
|
||||
</Button>
|
||||
</a>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex items-center space-x-2">
|
||||
<Link href="https://supabase.com/docs/guides/database/wrappers">
|
||||
<a target="_blank">
|
||||
<Button type="default" icon={<IconExternalLink />}>
|
||||
About Wrappers
|
||||
</Button>
|
||||
</a>
|
||||
</Link>
|
||||
<Tooltip.Root delayDuration={0}>
|
||||
<Tooltip.Trigger>
|
||||
<Button
|
||||
type="primary"
|
||||
loading={isEnabling}
|
||||
disabled={isNotAvailable || isEnabling || !canToggleWrappers}
|
||||
onClick={() => onEnableWrappers()}
|
||||
>
|
||||
Enable Wrappers
|
||||
</Button>
|
||||
</Tooltip.Trigger>
|
||||
{!canToggleWrappers && (
|
||||
<Tooltip.Content side="bottom">
|
||||
<Tooltip.Arrow className="radix-tooltip-arrow" />
|
||||
<div
|
||||
className={[
|
||||
'rounded bg-scale-100 py-1 px-2 leading-none shadow',
|
||||
'border border-scale-200',
|
||||
].join(' ')}
|
||||
>
|
||||
<span className="text-xs text-scale-1200">
|
||||
You need additional permissions to enable Wrappers for this project
|
||||
</span>
|
||||
</div>
|
||||
</Tooltip.Content>
|
||||
)}
|
||||
</Tooltip.Root>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default observer(WrappersDisabledState)
|
||||
@@ -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<Props> = ({ buttonText = 'Add wrapper', align = 'end' }) => {
|
||||
const { ref } = useParams()
|
||||
|
||||
return (
|
||||
<Dropdown
|
||||
side="bottom"
|
||||
align={align}
|
||||
size="small"
|
||||
overlay={
|
||||
<>
|
||||
{WRAPPERS.map((wrapper, idx) => (
|
||||
<Fragment key={idx}>
|
||||
<Link
|
||||
href={`/project/${ref}/database/wrappers/new?type=${wrapper.name.toLowerCase()}`}
|
||||
>
|
||||
<a>
|
||||
<Dropdown.Item
|
||||
key={wrapper.name}
|
||||
icon={
|
||||
<Image
|
||||
src={wrapper.icon}
|
||||
width={20}
|
||||
height={20}
|
||||
alt={`${wrapper.name} wrapper icon`}
|
||||
/>
|
||||
}
|
||||
>
|
||||
{wrapper.label}
|
||||
</Dropdown.Item>
|
||||
</a>
|
||||
</Link>
|
||||
{idx !== WRAPPERS.length - 1 && <Dropdown.Separator />}
|
||||
</Fragment>
|
||||
))}
|
||||
</>
|
||||
}
|
||||
>
|
||||
<Button type="primary" icon={<IconPlus strokeWidth={1.5} />}>
|
||||
{buttonText}
|
||||
</Button>
|
||||
</Dropdown>
|
||||
)
|
||||
}
|
||||
|
||||
export default observer(WrapperDropdown)
|
||||
@@ -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,
|
||||
|
||||
+35
-32
@@ -1,22 +1,35 @@
|
||||
import { FC, Fragment, useState } from 'react'
|
||||
import { IconCheck, IconHelpCircle, Modal } from 'ui'
|
||||
import { useFlag } from 'hooks'
|
||||
import { IconCheck, IconHelpCircle, IconInfo, Modal } from 'ui'
|
||||
import { PERMISSIONS_MAPPING } from './RolesHelperModal.constants'
|
||||
import * as Tooltip from '@radix-ui/react-tooltip'
|
||||
|
||||
interface Props {}
|
||||
|
||||
const RolesHelperModal: FC<Props> = ({}) => {
|
||||
const [showModal, setShowModal] = useState(false)
|
||||
const enableBillingOnlyReadOnlyRoles = useFlag('enableBillingOnlyReadOnlyRoles')
|
||||
|
||||
const permissionColumnClassName = [
|
||||
`${enableBillingOnlyReadOnlyRoles ? 'w-[40%]' : 'w-[49%]'}`,
|
||||
'text-sm pl-4 font-bold',
|
||||
].join(' ')
|
||||
const roleColumnClassName = [
|
||||
`${enableBillingOnlyReadOnlyRoles ? 'w-[12%]' : 'w-[17%]'}`,
|
||||
'text-sm h-8 flex items-center justify-center border-l border-scale-600 font-bold',
|
||||
].join(' ')
|
||||
const permissionColumnClassName = 'w-[40%] text-sm pl-4 font-bold'
|
||||
const roleColumnClassName =
|
||||
'w-[12%] text-sm h-8 flex items-center justify-center border-l border-scale-600 font-bold'
|
||||
|
||||
const accessTooltip = (
|
||||
<Tooltip.Root delayDuration={0}>
|
||||
<Tooltip.Trigger>
|
||||
<IconInfo size={14} strokeWidth={2} />
|
||||
</Tooltip.Trigger>
|
||||
<Tooltip.Content side="top">
|
||||
<Tooltip.Arrow className="radix-tooltip-arrow" />
|
||||
<div
|
||||
className={[
|
||||
'rounded bg-scale-100 py-1 px-2 leading-none shadow', // background
|
||||
'border border-scale-200 ', //border
|
||||
].join(' ')}
|
||||
>
|
||||
<span className="text-xs text-scale-1200">Only available in Team/Enterprise plan.</span>
|
||||
</div>
|
||||
</Tooltip.Content>
|
||||
</Tooltip.Root>
|
||||
)
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -30,7 +43,7 @@ const RolesHelperModal: FC<Props> = ({}) => {
|
||||
closable
|
||||
hideFooter
|
||||
visible={showModal}
|
||||
size={enableBillingOnlyReadOnlyRoles ? 'xxlarge' : 'xlarge'}
|
||||
size={'xxlarge'}
|
||||
header="Permissions for each role"
|
||||
onCancel={() => setShowModal(!showModal)}
|
||||
>
|
||||
@@ -48,12 +61,8 @@ const RolesHelperModal: FC<Props> = ({}) => {
|
||||
<div className={roleColumnClassName}>Owner</div>
|
||||
<div className={roleColumnClassName}>Adminstrator</div>
|
||||
<div className={roleColumnClassName}>Developer</div>
|
||||
{enableBillingOnlyReadOnlyRoles && (
|
||||
<div className={roleColumnClassName}>Read-only</div>
|
||||
)}
|
||||
{enableBillingOnlyReadOnlyRoles && (
|
||||
<div className={roleColumnClassName}>Billing-only</div>
|
||||
)}
|
||||
<div className={roleColumnClassName}>Read-only {accessTooltip}</div>
|
||||
<div className={roleColumnClassName}>Billing-only {accessTooltip}</div>
|
||||
</div>
|
||||
|
||||
<div className="max-h-[425px] overflow-y-auto">
|
||||
@@ -77,20 +86,14 @@ const RolesHelperModal: FC<Props> = ({}) => {
|
||||
<div className={roleColumnClassName}>
|
||||
{action.permissions.developer && <IconCheck size={14} strokeWidth={2} />}
|
||||
</div>
|
||||
{enableBillingOnlyReadOnlyRoles && (
|
||||
<div className={roleColumnClassName}>
|
||||
{action.permissions.read_only && (
|
||||
<IconCheck size={14} strokeWidth={2} />
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
{enableBillingOnlyReadOnlyRoles && (
|
||||
<div className={roleColumnClassName}>
|
||||
{action.permissions.billing_only && (
|
||||
<IconCheck size={14} strokeWidth={2} />
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
<div className={roleColumnClassName}>
|
||||
{action.permissions.read_only && <IconCheck size={14} strokeWidth={2} />}
|
||||
</div>
|
||||
<div className={roleColumnClassName}>
|
||||
{action.permissions.billing_only && (
|
||||
<IconCheck size={14} strokeWidth={2} />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</Fragment>
|
||||
|
||||
@@ -19,12 +19,8 @@ const TeamSettings = () => {
|
||||
const isOwner = ui.selectedOrganization?.is_owner
|
||||
|
||||
const { members } = useOrganizationDetail(slug || '')
|
||||
const { roles: allRoles } = useOrganizationRoles(slug)
|
||||
const { roles } = useOrganizationRoles(slug)
|
||||
|
||||
const enableBillingOnlyReadOnly = useFlag('enableBillingOnlyReadOnlyRoles')
|
||||
const roles = enableBillingOnlyReadOnly
|
||||
? allRoles
|
||||
: (allRoles ?? []).filter((role) => ['Owner', 'Administrator', 'Developer'].includes(role.name))
|
||||
const { rolesAddable } = getRolesManagementPermissions(roles)
|
||||
|
||||
const [isLeaving, setIsLeaving] = useState(false)
|
||||
|
||||
@@ -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<Props> = ({}) => {
|
||||
const { vault } = useStore()
|
||||
const { search } = useParams()
|
||||
|
||||
const [searchValue, setSearchValue] = useState<string>('')
|
||||
const [selectedSort, setSelectedSort] = useState<'updated_at' | 'name'>('updated_at')
|
||||
const [showAddSecretModal, setShowAddSecretModal] = useState(false)
|
||||
const [selectedSecretToEdit, setSelectedSecretToEdit] = useState<any>()
|
||||
const [selectedSecretToRemove, setSelectedSecretToRemove] = useState<any>()
|
||||
|
||||
useEffect(() => {
|
||||
if (search !== undefined) setSearchValue(search)
|
||||
}, [search])
|
||||
|
||||
const secrets = (
|
||||
searchValue.length > 0
|
||||
? vault.listSecrets(
|
||||
@@ -48,6 +54,19 @@ const SecretsManagement: FC<Props> = ({}) => {
|
||||
value={searchValue}
|
||||
onChange={(event) => setSearchValue(event.target.value)}
|
||||
icon={<IconSearch strokeWidth={2} size={16} />}
|
||||
actions={
|
||||
searchValue.length > 0
|
||||
? [
|
||||
<Button
|
||||
size="tiny"
|
||||
type="text"
|
||||
icon={<IconX />}
|
||||
className="px-1"
|
||||
onClick={() => setSearchValue('')}
|
||||
/>,
|
||||
]
|
||||
: []
|
||||
}
|
||||
/>
|
||||
<div className="w-32">
|
||||
<Listbox size="small" value={selectedSort} onChange={setSelectedSort}>
|
||||
|
||||
@@ -14,7 +14,7 @@ interface Props {
|
||||
}
|
||||
|
||||
const DatabaseLayout: FC<Props> = ({ title, children }) => {
|
||||
const { meta, ui, backups } = useStore()
|
||||
const { meta, ui, vault, backups } = useStore()
|
||||
const { isInitialized, isLoading, error } = meta.tables
|
||||
const project = ui.selectedProject
|
||||
|
||||
@@ -35,6 +35,7 @@ const DatabaseLayout: FC<Props> = ({ title, children }) => {
|
||||
meta.publications.load()
|
||||
|
||||
backups.load()
|
||||
vault.load()
|
||||
}
|
||||
}, [ui.selectedProject?.ref])
|
||||
|
||||
|
||||
@@ -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<FDWCreateVariables, 'wrapper' | 'formState' | 'newTables'>) {
|
||||
const newSchemasSql = newTables
|
||||
tables,
|
||||
}: Pick<FDWCreateVariables, 'wrapperMeta' | 'formState' | 'tables'>) {
|
||||
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(),
|
||||
])
|
||||
|
||||
|
||||
@@ -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<FDWDeleteVariables, 'wrapper'>) => {
|
||||
const encryptedOptions = wrapper.server.options.filter((option) => option.encrypted)
|
||||
export const getDeleteFDWSql = ({
|
||||
wrapper,
|
||||
wrapperMeta,
|
||||
}: Pick<FDWDeleteVariables, 'wrapper' | 'wrapperMeta'>) => {
|
||||
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<FDWDeleteVariables, 'wrapper'>
|
||||
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 })
|
||||
|
||||
|
||||
@@ -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<FDWUpdateVariables, 'wrapper' | 'wrapperMeta' | 'formState' | 'tables'>) => {
|
||||
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<ReturnType<typeof updateFDW>>
|
||||
|
||||
export const useFDWUpdateMutation = ({
|
||||
onSuccess,
|
||||
...options
|
||||
}: Omit<UseMutationOptions<FDWUpdateData, unknown, FDWUpdateVariables>, 'mutationFn'> = {}) => {
|
||||
const queryClient = useQueryClient()
|
||||
const { vault } = useStore()
|
||||
|
||||
return useMutation<FDWUpdateData, unknown, FDWUpdateVariables>((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,
|
||||
})
|
||||
}
|
||||
@@ -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[]
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
export function wrapWithTransaction(sql: string) {
|
||||
return /* SQL */ `
|
||||
begin;
|
||||
|
||||
${sql}
|
||||
|
||||
commit;
|
||||
`
|
||||
}
|
||||
@@ -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'
|
||||
|
||||
Loaded 100 of 105 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user