Merge branch 'master' into chore/vite

This commit is contained in:
Alaister Young committed 2024-07-01 13:07:47 +08:00
commit bd97dedd20
30 files changed
+692 -103

No files matched your search

+1 -1
View File
@@ -40,6 +40,6 @@
"autoprefixer": "^10.4.14",
"nextjs-node-loader": "^1.1.5",
"postcss": "^8.4.31",
"typescript": "^5.4.3"
"typescript": "^5.5.0"
}
}
+1 -1
View File
@@ -52,7 +52,7 @@
"shiki": "^1.1.7",
"tailwindcss": "^3.3.0",
"tsconfig": "*",
"typescript": "^5",
"typescript": "^5.5.0",
"unist-builder": "3.0.0"
}
}
+14 -14
View File
@@ -122,22 +122,22 @@ const selfHostingOptions = [
{
title: 'Auth',
icon: 'auth',
href: 'reference/self-hosting-auth/introduction',
href: '/reference/self-hosting-auth/introduction',
},
{
title: 'Realtime',
icon: 'realtime',
href: 'reference/self-hosting-realtime/introduction',
href: '/reference/self-hosting-realtime/introduction',
},
{
title: 'Storage',
icon: 'storage',
href: 'reference/self-hosting-storage/introduction',
href: '/reference/self-hosting-storage/introduction',
},
{
title: 'Analytics',
icon: 'analytics',
href: 'reference/self-hosting-analytics/introduction',
href: '/reference/self-hosting-analytics/introduction',
},
]
@@ -145,32 +145,32 @@ const clientLibraries = [
{
title: 'Javascript',
icon: 'reference-javascript',
href: 'reference/javascript/introduction',
href: '/reference/javascript/introduction',
},
{
title: 'Flutter',
icon: 'reference-dart',
href: 'reference/dart/introduction',
href: '/reference/dart/introduction',
},
{
title: 'Python',
icon: 'reference-python',
href: 'reference/python/introduction',
href: '/reference/python/introduction',
},
{
title: 'C#',
icon: 'reference-csharp',
href: 'reference/csharp/introduction',
href: '/reference/csharp/introduction',
},
{
title: 'Swift',
icon: 'reference-swift',
href: 'reference/swift/introduction',
href: '/reference/swift/introduction',
},
{
title: 'Kotlin',
icon: 'reference-kotlin',
href: 'reference/kotlin/introduction',
href: '/reference/kotlin/introduction',
},
]
@@ -179,25 +179,25 @@ const additionalResources = [
title: 'Management API',
description: 'Manage your Supabase projects and organizations.',
icon: 'reference-api',
href: 'reference/api/introduction',
href: '/reference/api/introduction',
},
{
title: 'Supabase CLI',
description: 'Use the CLI to develop, manage and deploy your projects.',
icon: 'reference-cli',
href: 'reference/cli/introduction',
href: '/reference/cli/introduction',
},
{
title: 'Platform Guides',
description: 'Learn more about the tools and services powering Supabase.',
icon: 'platform',
href: 'guides/platform',
href: '/guides/platform',
},
{
title: 'Integrations',
description: 'Explore a variety of integrations from Supabase partners.',
icon: 'integrations',
href: 'guides/integrations',
href: '/guides/integrations',
},
]
@@ -243,11 +243,15 @@ export async function updateSession(request: NextRequest) {
// supabase.auth.getUser(). A simple mistake could make it very hard to debug
// issues with users being randomly logged out.
const { data: user, error } = await supabase.auth.getUser()
const {
data: { user },
} = await supabase.auth.getUser()
if (!user) {
if (!user && !request.nextUrl.pathname.startsWith('/login')) {
// no user, potentially respond by redirecting the user to the login page
return NextResponse.redirect('/login')
const url = request.nextUrl.clone()
url.pathname = '/login'
return NextResponse.redirect(url)
}
// IMPORTANT: You *must* return the supabaseResponse object as it is. If you're
@@ -35,7 +35,7 @@ Examples:
- A `subscriptions` table which maps to your Stripe subscriptions.
- A `collections` table which maps to a Firebase collection.
Although a Foreign Table behaves like any other table, the data is not stored inside your database. The data remains inside the Remote Server.
Although a foreign table behaves like any other table, the data is not stored inside your database. The data remains inside the Remote Server.
### ETL with Wrappers
@@ -111,9 +111,89 @@ This process can be taxing on your database if you are moving large amounts of d
## Security
Foreign Data Wrappers do not provide Row Level Security. We do not advise exposing them via your API. Wrappers should _always_ be stored in a private schema. For example, if you are connecting to your Stripe account, you should create a `stripe` schema to store all of your Foreign Tables inside. This schema should _not_ be added to the “Additional Schemas” setting in the API section.
Foreign Data Wrappers do not provide Row Level Security, thus it is not advised to expose them via your API. Wrappers should _always_ be stored in a private schema. For example, if you are connecting to your Stripe account, you should create a `stripe` schema to store all of your foreign tables inside. This schema should _not_ be added to the “Additional Schemas” setting in the API section.
If you want to expose any of the columns to your public API, create a [View](https://supabase.com/docs/guides/database/tables#views) in the `public` schema. You can also interact with your Foreign Tables inside a [Database Function](https://supabase.com/docs/guides/database/functions).
If you want to expose any of the foreign table columns to your public API, you can create a [Database Function with security definer](https://supabase.com/docs/guides/database/functions#security-definer-vs-invoker) in the `public` schema, and then you can interact with your foreign table through API. For better access control, the function should have appropriate filters on the foreign table to apply security rules based on your business needs.
As an example, go to [SQL Editor](https://supabase.com/dashboard/project/_/sql/new) and then follow below steps,
1. Create a Stripe Products foreign table:
```sql
create foreign table stripe.stripe_products (
id text,
name text,
active bool,
default_price text,
description text,
created timestamp,
updated timestamp,
attrs jsonb
)
server stripe_fdw_server
options (
object 'products',
rowid_column 'id'
);
```
2. Create a security definer function that queries the foreign table and filters on the name prefix parameter:
```sql
create function public.get_stripe_products(name_prefix text)
returns table (
id text,
name text,
active boolean,
default_price text,
description text
)
language plpgsql
security definer set search_path = ''
as $$
begin
return query
select
t.id,
t.name,
t.active,
t.default_price,
t.description
from
stripe.stripe_products t
where
t.name like name_prefix || '%'
;
end;
$$;
```
3. Restrict the function execution to a specific role only, for example, the authenticated users:
<Admonition type="danger">
By default, the function created can be executed by any roles like `anon`, that means the
foreign table is public accessible. Always limit the function execution permission to
appropriate roles.
</Admonition>
```sql
-- revoke public execute permission
revoke execute on function public.get_stripe_products from public;
revoke execute on function public.get_stripe_products from anon;
-- grant execute permission to a specific role only
grant execute on function public.get_stripe_products to authenticated;
```
Once the preceding steps are finished, the function can be invoked from Supabase client to query the foreign table:
```js
const { data, error } = await supabase
.rpc('get_stripe_products', { name_prefix: 'Test' })
.select('*')
if (error) console.error(error)
else console.log(data)
```
## Resources
@@ -15,7 +15,7 @@ Read more on [how billing is organized](https://supabase.com/docs/guides/platfor
#### How many free projects can I have?
You are entitled to two active free projects. Paused projects do not count towards your quota. Note that within an organization, we count the free project limits from all members that are either Owner or Admin. If you’ve got another organization member with the Admin or Owner role that has already exhausted their free project quota, you won’t be able to launch another free project in that organization. You can create simply create another free plan organization or change the role of the affected member in your [organization’s team settings](https://supabase.com/dashboard/org/_/team).
You are entitled to two active free projects. Paused projects do not count towards your quota. Note that within an organization, we count the free project limits from all members that are either Owner or Admin. If you’ve got another organization member with the Admin or Owner role that has already exhausted their free project quota, you won’t be able to launch another free project in that organization. You can create another free plan organization or change the role of the affected member in your [organization’s team settings](https://supabase.com/dashboard/org/_/team).
#### Can I mix free and paid projects in a single organization?
@@ -86,15 +86,45 @@ We can transfer the amount as credits to another organization of your choice. Yo
#### What will happen when I exceed the free plan quota?
When you exceed the Free plan quota, we will monitor and notify you about the exceeding limits. It is important to take action at this point. If you continue to exceed the limits without reducing your usage, we will pause your projects. To avoid project pausing, you have two options: reduce your usage or upgrade to a paid plan.
You will be notified when you exceed the free plan quota. It is important to take action at this point. If you continue to exceed the limits without reducing your usage, service restrictions will apply. To avoid service restrictions, you have two options: reduce your usage or upgrade to a paid plan. Learn more about restrictions in the [Fair Use Policy](#fair-use-policy) section.
#### What will happen when I exceed the Pro plan quota and have the spend cap on?
When you exceed the Pro plan quota, we will monitor and notify you about the exceeding limits. To unblock yourself, you can simply toggle off your spend cap in your [organization’s billing settings](https://supabase.com/dashboard/org/_/billing) to pay for over-usage beyond the Pro plans limits. If you continue to exceed the limits without reducing your usage or turning off the spend cap, we will pause your projects.
You will be notified when you exceed your Pro plan quota. To unblock yourself, you can toggle off your spend cap in your [organization’s billing settings](https://supabase.com/dashboard/org/_/billing) to pay for over-usage beyond the Pro plans limits. If you continue to exceed the limits without reducing your usage or turning off the spend cap, restrictions will apply. Learn more about restrictions in the [Fair Use Policy](#fair-use-policy) section.
#### How do I scale beyond the limits of my Pro plan?
The Pro plan has a spend cap enabled by default to keep costs under control. If you want to scale beyond the plans included quota, ~~simply~~ switch off the spend cap to pay for additional usage beyond the plans included limits. You can toggle the spend cap in the [organization's billing settings](https://supabase.com/dashboard/org/_/billing). Read more about the [spend cap](https://supabase.com/docs/guides/platform/spend-cap).
The Pro plan has a spend cap enabled by default to keep costs under control. If you want to scale beyond the plans included quota, switch off the spend cap to pay for additional usage beyond the plans included limits. You can toggle the spend cap in the [organization's billing settings](https://supabase.com/dashboard/org/_/billing). Read more about the [spend cap](https://supabase.com/docs/guides/platform/spend-cap).
## Fair Use Policy
#### What is the Fair Use Policy?
Our Fair Use Policy gives developers the freedom to build and experiment with Supabase, while protecting our infrastructure. Under the Fair Use policy, service restrictions may apply to your organization if:
- You continually exceed the free plan quota
- You continually exceed Pro plan quota and have the spend cap enabled
- You have overdue invoices
- You have an expired credit card
You will receive a notification before Fair Use Policy restrictions are applied. However, in some cases, like suspected abuse of our services, restrictions may be applied without prior notice.
#### How is the Fair Use Policy applied?
The Fair Use Policy is applied through service restrictions. This could mean:
- Pausing projects
- Switching databases to read-only mode
- Disabling new project launches/transfers
- Responding with a 402 status code for all API requests
The Fair Use Policy is generally applied to all projects of the restricted organization.
#### How can I remove restrictions applied from the Fair Use Policy?
To remove restrictions, you will need to address the issue that caused the restriction. This could be reducing your usage, paying overdue invoices, updating your payment method, or any other issue that caused the restriction. Once the issue is resolved, the restriction will be lifted.
Restrictions due to usage limits are lifted with the next billing cycle as your quota refills at the beginning of each cycle. You can see when your current billing cycle ends on the [billing page](https://supabase.com/dashboard/org/_/usage) under "Billing Breakdown". If your organization is on the free plan, you can also lift restrictions immediately by [upgrading](https://supabase.com/dashboard/org/_/billing?panel=subscriptionPlan) to Pro.
## Reports and invoices
@@ -17,7 +17,7 @@ Returns the name of a file. For example, if your file is stored in `public/subfo
This example demonstrates how you would allow any user to download a file called `favicon.ico`:
```sql
create policy "Allow authenticated uploads"
create policy "Allow public downloads"
on storage.objects
for select
to public
+1 -1
View File
@@ -103,6 +103,6 @@
"simple-git": "^3.24.0",
"tsconfig": "*",
"tsx": "^4.6.2",
"typescript": "^5.4.3"
"typescript": "^5.5.0"
}
}
+1
View File
@@ -18,6 +18,7 @@ Chase Granberry
Chris Caruso
Chris Copplestone
Chris Gwilliams
Craig Cannon
Dave Wilson
Deji I
Div Arora
+157 -6
View File
@@ -138,6 +138,32 @@ functions:
- When both **Confirm email** and **Confirm phone** (even when phone provider is disabled) are enabled in [your project](/dashboard/project/_/auth/providers), an obfuscated/fake user object is returned.
- When either **Confirm email** or **Confirm phone** (even when phone provider is disabled) is disabled, the error message, `User already registered` is returned.
- To fetch the currently logged-in user, refer to [`getUser()`](/docs/reference/swift/get-user).
overwriteParams:
- name: email
isOptional: true
type: String
description: One of `email` or `phone` must be provided.
- name: phone
isOptional: true
type: String
description: One of `email` or `phone` must be provided.
- name: password
type: String
- name: data
isOptional: true
type: JSONObject
description: >
A custom data object to store additional user metadata.
- name: redirectTo
isOptional: true
type: URL
description: >
Only for email signups.
The redirect URL embedded in the email link.
Must be a configured redirect URL for your Supabase instance.
- name: captchaToken
isOptional: true
type: String
examples:
- id: sign-up
name: Sign up with email and password
@@ -232,7 +258,6 @@ functions:
- `PASSWORD_RECOVERY`
- Emitted instead of the `SIGNED_IN` event when the user lands on a page that includes a password recovery link in the URL.
- Use it to show a UI to the user where they can [reset their password](/docs/guides/auth/passwords#resetting-a-users-password-forgot-password).
examples:
- id: listen-to-auth-changes
name: Listen to auth changes
@@ -292,6 +317,20 @@ functions:
title: "signInWithPassword()"
notes: |
- Requires either an email and password or a phone number and password.
overwriteParams:
- name: email
isOptional: true
type: String
description: One of `email` or `phone` must be provided.
- name: phone
isOptional: true
type: String
description: One of `email` or `phone` must be provided.
- name: password
type: String
- name: captchaToken
isOptional: true
type: String
examples:
- id: sign-in-with-email-and-password
name: Sign in with email and password
@@ -322,6 +361,42 @@ functions:
```
- id: sign-in-with-otp
title: "signInWithOTP()"
overwriteParams:
- name: email
isOptional: true
type: String
description: One of `email` or `phone` must be provided.
- name: phone
isOptional: true
type: String
description: One of `email` or `phone` must be provided.
- name: redirectTo
isOptional: true
type: String
description: >
Only for email signups.
The redirect URL embedded in the email link.
Must be a configured redirect URL for your Supabase instance.
- name: channel
isOptional: true
type: MessagingChannel
description: >
The channel to use for sending messages.
Only for phone signups.
- name: shouldCreateUser
isOptional: true
type: Bool
description: >
Whether to create the user if they don't already exist.
Defaults to true.
- name: data
isOptional: true
type: JSONObject
description: >
A custom data object to store additional user metadata.
- name: captchaToken
isOptional: true
type: String
notes: |
- Requires either an email or phone number.
- This method is used for passwordless sign-ins where a OTP is sent to the user's email or phone number.
@@ -365,6 +440,31 @@ functions:
```
- id: sign-in-with-oauth
title: "signInWithOAuth()"
overwriteParams:
- name: provider
type: Provider
description: >
The third-party provider.
- name: redirectTo
type: URL
isOptional: true
description: >
A URL to send the user to after they are confirmed.
- name: scopes
type: String
isOptional: true
description: >
A space-separated list of scopes granted to the OAuth application.
- name: queryParams
type: "[(name: String, value: String?)]"
isOptional: true
description: >
Additional query params.
- name: configure
type: Callback
isOptional: true
description: >
A custom configuration callback for opening the OAuth flow externally.
notes: |
- This method is used for signing in using a third-party provider.
- Supabase supports many different [third-party providers](https://supabase.com/docs/guides/auth#providers).
@@ -454,7 +554,27 @@ functions:
)
```
- id: sign-in-with-sso
title: 'signInWithSSO()'
title: "signInWithSSO()"
overwriteParams:
- name: providerId
isOptional: true
type: String
description: >
UUID of the SSO provider.
One of `providerId` or `domain` is required.
- name: domain
isOptional: true
type: String
description: >
Domain name of the organization to use SSO with.
One of `providerId` or `domain` is required.
- name: redirectTo
type: String
description: >
The URL to redirect the user to after they have signed in.
Must be a configured redirect URL for your Supabase instance.
- name: captchaToken
type: String
notes: |
- Before you can call this method you need to [establish a connection](/docs/guides/auth/sso/auth-sso-saml#managing-saml-20-connections) to an identity provider. Use the [CLI commands](/docs/reference/cli/supabase-sso) to do this.
- If you've associated an email domain to the identity provider, you can use the `domain` property to start a sign-in flow.
@@ -506,6 +626,37 @@ functions:
```
- id: verify-otp
title: "verifyOTP()"
overwriteParams:
- name: email
isOptional: true
type: String
description: One of `phone`, `email`, or `token_hash` must be provided.
- name: phone
isOptional: true
type: String
description: One of `phone`, `email`, or `token_hash` must be provided.
- name: token_hash
isOptional: true
type: String
description: >
The token hash from the user's email link.
One of `phone`, `email`, or `token_hash` must be provided.
- name: type
type: EmailOTPType | MobileOTPType
- name: token
isOptional: true
type: String
description: The OTP sent to the user. Required if using `phone` or `email`.
- name: redirectTo
isOptional: true
type: URL
description: >
A URL to redirect the user to after they are confirmed.
Must be in your configured redirect URLs.
- name: captchaToken
isOptional: true
type: String
description: Deprecated.
notes: |
- The `verifyOTP` method takes in different verification types. If a phone number is used, the type can either be `sms` or `phone_change`. If an email address is used, the type can be one of the following: `signup`, `magiclink`, `recovery`, `invite`, `email_change`, or `email`.
- The verification type used should be determined based on the corresponding auth method called before `verifyOTP` to sign up / sign-in a user.
@@ -629,7 +780,7 @@ functions:
let identities = try await supabase.auth.userIdentities()
```
- id: link-identity
title: 'linkIdentity()'
title: "linkIdentity()"
notes: |
- The **Enable Manual Linking** option must be enabled from your [project's authentication settings](/dashboard/project/_/settings/auth).
- The user needs to be signed in to call `linkIdentity()`.
@@ -657,7 +808,7 @@ functions:
}
```
- id: unlink-identity
title: 'unlinkIdentity()'
title: "unlinkIdentity()"
notes: |
- The **Enable Manual Linking** option must be enabled from your [project's authentication settings](/dashboard/project/_/settings/auth).
- The user needs to be signed in to call `unlinkIdentity()`.
@@ -928,7 +1079,7 @@ functions:
let factors = try await supabase.auth.mfa.listFactors()
```
- id: admin-api
title: 'Overview'
title: "Overview"
notes: |
- Any method under the `supabase.auth.admin` namespace requires a `service_role` key.
- These methods are considered admin methods and should be called on a trusted server. Never expose your `service_role` key in the browser.
@@ -949,7 +1100,7 @@ functions:
let adminAuthClient = supabase.auth.admin
```
- id: delete-user
title: 'deleteUser()'
title: "deleteUser()"
notes: |
- The `deleteUser()` method requires the user's ID, which maps to the `auth.users.id` column.
examples:
@@ -99,9 +99,9 @@ const CreateIndexSidePanel = ({ visible, onClose }: CreateIndexSidePanelProps) =
})
const generatedSQL = `
CREATE INDEX ON "${selectedSchema}"."${selectedEntity}" USING ${selectedIndexType} (${selectedColumns.join(
', '
)});
CREATE INDEX ON "${selectedSchema}"."${selectedEntity}" USING ${selectedIndexType} (${selectedColumns
.map((column) => `"${column}"`)
.join(', ')});
`.trim()
const onSaveIndex = () => {
@@ -206,6 +206,48 @@ export const MOBILES: ConnectionType[] = [
},
],
},
{
key: 'ionicreact',
label: 'Ionic React',
icon: 'react',
guideLink: 'https://supabase.com/docs/guides/getting-started/tutorials/with-ionic-react',
children: [
{
key: 'supabasejs',
label: 'Supabase-js',
children: [],
icon: 'supabase',
},
],
},
{
key: 'swift',
label: 'Swift',
icon: 'swift',
guideLink: 'https://supabase.com/docs/guides/getting-started/tutorials/with-swift',
children: [
{
key: 'supabaseswift',
label: 'supabase-swift',
children: [],
icon: 'supabase',
},
],
},
{
key: 'androidkotlin',
label: 'Android Kotlin',
icon: 'kotlin',
guideLink: 'https://supabase.com/docs/guides/getting-started/tutorials/with-kotlin',
children: [
{
key: 'supabasekt',
label: 'supabase-kt',
children: [],
icon: 'supabase',
},
],
},
]
export const ORMS: ConnectionType[] = [
@@ -0,0 +1,83 @@
import type { ContentFileProps } from 'components/interfaces/Home/Connect/Connect.types'
import {
ConnectTabs,
ConnectTabTriggers,
ConnectTabTrigger,
ConnectTabContent,
} from 'components/interfaces/Home/Connect/ConnectTabs'
import SimpleCodeBlock from 'components/to-be-cleaned/SimpleCodeBlock'
const ContentFile = ({ projectKeys }: ContentFileProps) => {
return (
<ConnectTabs>
<ConnectTabTriggers>
<ConnectTabTrigger value="MainActivity.kt" />
<ConnectTabTrigger value="TodoItem.kt" />
</ConnectTabTriggers>
<ConnectTabContent value="TodoItem.kt">
<SimpleCodeBlock className="kotlin" parentClassName="min-h-72">
{`
@Serializable
data class TodoItem(val id: Int, val name: String)
`}
</SimpleCodeBlock>
</ConnectTabContent>
<ConnectTabContent value="MainActivity.kt">
<SimpleCodeBlock className="kotlin" parentClassName="min-h-72">
{`
val supabase = createSupabaseClient(
supabaseUrl = "${projectKeys.apiUrl ?? 'your-project-url'}",
supabaseKey = "${projectKeys.anonKey ?? 'your-anon-key'}"
) {
install(Postgrest)
}
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
MaterialTheme {
// A surface container using the 'background' color from the theme
Surface(
modifier = Modifier.fillMaxSize(),
color = MaterialTheme.colorScheme.background
) {
TodoList()
}
}
}
}
}
@Composable
fun TodoList() {
var items by remember { mutableStateOf<List<TodoItem>>(listOf()) }
LaunchedEffect(Unit) {
withContext(Dispatchers.IO) {
items = supabase.from("todos")
.select().decodeList<TodoItem>()
}
}
LazyColumn {
items(
items,
key = { item -> item.id },
) { item ->
Text(
item.name,
modifier = Modifier.padding(8.dp),
)
}
}
}
`}
</SimpleCodeBlock>
</ConnectTabContent>
</ConnectTabs>
)
}
export default ContentFile
@@ -0,0 +1,115 @@
import type { ContentFileProps } from 'components/interfaces/Home/Connect/Connect.types'
import {
ConnectTabs,
ConnectTabTriggers,
ConnectTabTrigger,
ConnectTabContent,
} from 'components/interfaces/Home/Connect/ConnectTabs'
import SimpleCodeBlock from 'components/to-be-cleaned/SimpleCodeBlock'
const ContentFile = ({ projectKeys }: ContentFileProps) => {
return (
<ConnectTabs>
<ConnectTabTriggers>
<ConnectTabTrigger value=".env" />
<ConnectTabTrigger value="src/supabaseClient.tsx" />
<ConnectTabTrigger value="src/App.tsx" />
</ConnectTabTriggers>
<ConnectTabContent value=".env">
<SimpleCodeBlock className="bash" parentClassName="min-h-72">
{`
REACT_APP_SUPABASE_URL=${projectKeys.apiUrl ?? 'your-project-url'}
REACT_APP_SUPABASE_ANON_KEY=${projectKeys.anonKey ?? 'your-anon-key'}
`}
</SimpleCodeBlock>
</ConnectTabContent>
<ConnectTabContent value="src/supabaseClient.tsx">
<SimpleCodeBlock className="ts" parentClassName="min-h-72">
{`
import { createClient } from '@supabase/supabase-js'
const supabaseUrl = process.env.REACT_APP_SUPABASE_URL
const supabaseAnonKey = process.env.REACT_APP_SUPABASE_ANON_KEY
export const supabase = createClient(supabaseUrl, supabaseAnonKey)
`}
</SimpleCodeBlock>
</ConnectTabContent>
<ConnectTabContent value="src/App.tsx">
<SimpleCodeBlock className="ts" parentClassName="min-h-72">
{`
import React, { useEffect, useState } from 'react';
import { setupIonicReact, IonApp } from '@ionic/react';
import {
IonContent,
IonHeader,
IonTitle,
IonToolbar,
IonList,
IonItem,
} from '@ionic/react';
/* Core CSS required for Ionic components to work properly */
import '@ionic/react/css/core.css';
/* Theme variables */
import './theme/variables.css';
import { supabase } from './supabaseClient';
setupIonicReact();
export default function App() {
const [todos, setTodos] = useState([]);
useEffect(() => {
getTodos();
}, []);
const getTodos = async () => {
try {
const { data, error } = await supabase.from('todos').select();
if (error) {
console.error('Error fetching todos:', error.message);
return;
}
if (data) {
setTodos(data);
}
} catch (error) {
console.error('Error fetching todos:', error.message);
}
};
return (
<IonApp>
<>
<IonHeader>
<IonToolbar>
<IonTitle>Todos</IonTitle>
</IonToolbar>
</IonHeader>
<IonContent>
<IonList>
{todos.map((todo) => (
<IonItem key={todo.id}>{todo.title}</IonItem>
))}
</IonList>
</IonContent>
</>
</IonApp>
);
}
`}
</SimpleCodeBlock>
</ConnectTabContent>
</ConnectTabs>
)
}
export default ContentFile
@@ -33,7 +33,7 @@ SUPABASE_ANON_KEY=${projectKeys.anonKey ?? 'your-anon-key'}
import { createClient } from "@supabase/supabase-js";
const supabaseUrl = process.env.SUPABASE_URL;
const supabaseKey = process.env.SUPABASE_KEY;
const supabaseKey = process.env.SUPABASE_ANON_KEY;
export const supabase = createClient(supabaseUrl, supabaseKey);
`}
@@ -0,0 +1,84 @@
import type { ContentFileProps } from 'components/interfaces/Home/Connect/Connect.types'
import {
ConnectTabs,
ConnectTabTriggers,
ConnectTabTrigger,
ConnectTabContent,
} from 'components/interfaces/Home/Connect/ConnectTabs'
import SimpleCodeBlock from 'components/to-be-cleaned/SimpleCodeBlock'
const ContentFile = ({ projectKeys }: ContentFileProps) => {
return (
<ConnectTabs>
<ConnectTabTriggers>
<ConnectTabTrigger value="Supabase.swift" />
<ConnectTabTrigger value="Todo.swift" />
<ConnectTabTrigger value="ContentView.swift" />
</ConnectTabTriggers>
<ConnectTabContent value="Supabase.swift">
<SimpleCodeBlock className="swift" parentClassName="min-h-72">
{`
import Foundation
import Supabase
let supabase = SupabaseClient(
supabaseURL: URL(string: "${projectKeys.apiUrl ?? 'your-project-url'}")!,
supabaseKey: "${projectKeys.anonKey ?? 'your-anon-key'}"
)
`}
</SimpleCodeBlock>
</ConnectTabContent>
<ConnectTabContent value="Todo.swift">
<SimpleCodeBlock className="swift" parentClassName="min-h-72">
{`
import Foundation
struct Todo: Identifiable, Decodable {
var id: Int
var title: String
}
`}
</SimpleCodeBlock>
</ConnectTabContent>
<ConnectTabContent value="ContentView.swift">
<SimpleCodeBlock className="swift" parentClassName="min-h-72">
{`
import Supabase
import SwiftUI
struct ContentView: View {
@State var todos: [Todo] = []
var body: some View {
NavigationStack {
List(todos) { todo in
Text(todo.title)
}
.navigationTitle("Todos")
.task {
do {
todos = try await supabase.from("todos").select().execute().value
} catch {
debugPrint(error)
}
}
}
}
}
#Preview {
ContentView()
}
`}
</SimpleCodeBlock>
</ConnectTabContent>
</ConnectTabs>
)
}
export default ContentFile
@@ -41,9 +41,11 @@ export const sanitizeRoute = (route: string, routerQueries: ParsedUrlQuery) => {
// [Joshen] Ideally we shouldn't use hard coded numbers, but temp workaround
// for storage bucket route since its longer
const isStorageBucketRoute = 'bucketId' in routerQueries
const isSecurityAdvisorRoute = 'preset' in routerQueries
return route
.split('/')
.slice(0, isStorageBucketRoute ? 5 : 4)
.slice(0, isStorageBucketRoute || isSecurityAdvisorRoute ? 5 : 4)
.join('/')
} else {
return route
+1 -1
View File
@@ -168,7 +168,7 @@
"sass": "^1.77.6",
"storybook-dark-mode": "^3.0.1",
"tailwindcss": "^3.4.1",
"typescript": "^5.4.3",
"typescript": "^5.5.0",
"vite": "^5.3.2",
"vite-plugin-cjs-interop": "^2.1.1"
}
@@ -32,7 +32,7 @@ Using PostGIS we can then programmatically generate vector tiles and serve them
Vector tiles are packets of geographic data, packaged into pre-defined roughly-square shaped "tiles" for transfer over the web. Map data is requested by a client as a set of "tiles" corresponding to square areas of land of a pre-defined size and location.
Especially for large datasets, this has the benefit that the data transfer is greatly reduced because because only data within the current viewport, and at the current zoom level needs to be transferred.
Especially for large datasets, this has the benefit that the data transfer is greatly reduced because only data within the current viewport, and at the current zoom level needs to be transferred.
</Admonition>
@@ -71,7 +71,7 @@ PG_USE_COPY=true ogr2ogr -f pgdump places.sql places.geojson
Enable the PostGIS extension on your Supabase Database on a dedicated separate `gis` schema. To do so you can navigate to the [SQL Editor](/dashboard/project/_/sql/new) and run the following SQL, or you can enable the extension from the [Database Extensions Settings](/dashboard/project/_/database/extensions).
As PostGIS can be quite compute heavy, we recommend enabling it on a decidacted separate schema, for example names `gis`!
As PostGIS can be quite compute heavy, we recommend enabling it on a dedicated separate schema, for example, named `gis`!
```sql
CREATE SCHEMA IF NOT EXISTS "gis";
@@ -149,14 +149,14 @@ END;
$$;
```
To limit the amount of data send over the wire, we limit the amount of metadata to include in the vector tile. For example we add a condition for the zoom level, and only return the place name when the user has zoomed in beyond level 13.
To limit the amount of data sent over the wire, we limit the amount of metadata to include in the vector tile. For example we add a condition for the zoom level, and only return the place name when the user has zoomed in beyond level 13.
## Use supabase-js to fetch vector tiles from MapLibre GL client
You can find the full `index.html` code on [GitHub](https://github.com/bdon/supabase-vector-tile/blob/main/index.html). Here we'll highlight how to add a new protocol to MapLibreGL to fetch the bas64 encoded binary vector tile data via supabase-js so that MapLibre GL can fetch and render the data as your users interact with the map:
```js index.html
const client = supabase.createClient('you-supabase-api-url', 'your-supabase-anon-key')
const client = supabase.createClient('your-supabase-api-url', 'your-supabase-anon-key')
function base64ToArrayBuffer(base64) {
var binaryString = atob(base64)
@@ -213,7 +213,7 @@ const map = new maplibregl.Map({
## On demand fetch additional JSON metadata
To limit the amount of data send over the wire, we don't encode all the metadata in the vector tile itself, but rather set up an onclick handler to fetch the additional metadata on demand within the MapLibre GL popup:
To limit the amount of data sent over the wire, we don't encode all the metadata in the vector tile itself, but rather set up an onclick handler to fetch the additional metadata on demand within the MapLibre GL popup:
```js index.html
// ..
+1 -1
View File
@@ -68,7 +68,7 @@
"swiper": "^11.0.7",
"three": "^0.136.0",
"typed.js": "^2.0.16",
"typescript": "^5.4.3",
"typescript": "^5.5.0",
"ui": "*",
"ui-patterns": "*",
"use-debounce": "^7.0.1",
@@ -41,25 +41,23 @@ We'll be using:
```sql
CREATE TABLE public.rooms (
id bigint GENERATED BY default AS IDENTITY PRIMARY KEY,
topic text NOT NULL
topic text NOT NULL UNIQUE
);
CREATE UNIQUE INDEX "unique_topic" on public.rooms (topic);
ALTER TABLE public.rooms ENABLE ROW LEVEL SECURITY;
CREATE TABLE public.profiles (
id bigint GENERATED BY default AS IDENTITY PRIMARY KEY,
id uuid NOT NULL REFERENCES auth.users ON DELETE CASCADE,
email text NOT NULL,
user_id uuid REFERENCES auth.users (id)
PRIMARY KEY (id)
);
ALTER TABLE public.profiles ENABLE ROW LEVEL SECURITY;
CREATE TABLE public.rooms_users (
user_id uuid REFERENCES auth.users (id),
room_id bigint REFERENCES public.rooms (id),
room_topic text REFERENCES public.rooms (topic),
created_at timestamptz DEFAULT CURRENT_TIMESTAMP
);
ALTER TABLE public.rooms ENABLE ROW LEVEL SECURITY;
ALTER TABLE public.profiles ENABLE ROW LEVEL SECURITY;
ALTER TABLE public.rooms_users ENABLE ROW LEVEL SECURITY;
```
@@ -82,7 +80,6 @@ AS PERMISSIVE FOR INSERT
TO supabase_auth_admin
WITH CHECK (true);
CREATE POLICY "authenticated users can read rooms"
ON "public"."rooms"
AS PERMISSIVE FOR SELECT
@@ -95,7 +92,6 @@ AS PERMISSIVE FOR INSERT
TO authenticated
WITH CHECK (TRUE);
CREATE POLICY "authenticated users can read rooms_users"
ON "public"."rooms_users"
AS PERMISSIVE FOR SELECT
@@ -108,30 +104,30 @@ AS PERMISSIVE FOR INSERT
TO authenticated
WITH CHECK (TRUE);
CREATE POLICY "authenticated user can enter room"
CREATE POLICY "authenticated user can read broadcast messages and presence state"
ON "realtime"."messages"
AS PERMISSIVE FOR SELECT
TO authenticated
USING (
EXISTS (
SELECT ru.user_id
FROM public.rooms_users ru
JOIN public.rooms r ON r.id = ru.room_id AND r.topic = realtime.topic()
WHERE ru.user_id = auth.uid()
SELECT 1
FROM public.rooms_users
WHERE user_id = auth.uid()
AND room_topic = realtime.topic()
AND realtime.messages.extension in ('broadcast', 'presence')
)
);
CREATE POLICY "authenticated user can broadcast in room"
CREATE POLICY "authenticated user can send broadcast messages and track presence"
ON "realtime"."messages"
AS PERMISSIVE FOR INSERT
TO authenticated
WITH CHECK (
EXISTS (
SELECT ru.user_id
FROM public.rooms_users ru
JOIN public.rooms r ON r.id = ru.room_id AND r.topic = realtime.topic()
WHERE ru.user_id = auth.uid()
SELECT 1
FROM public.rooms_users
WHERE user_id = auth.uid()
AND room_topic = realtime.topic()
AND realtime.messages.extension in ('broadcast', 'presence')
)
);
@@ -145,7 +141,7 @@ We need to create a database function and trigger to add an entry to `public.pro
CREATE OR REPLACE FUNCTION insert_user() RETURNS TRIGGER AS
$$
BEGIN
INSERT INTO public.profiles (user_id, email) VALUES (NEW.id, NEW.email); RETURN NEW;
INSERT INTO public.profiles (id, email) VALUES (NEW.id, NEW.email); RETURN NEW;
END;
$$ LANGUAGE plpgsql;
@@ -159,18 +155,19 @@ GRANT INSERT ON TABLE public.profiles TO supabase_auth_admin;
## Coding Concerns
* Check we're using the `2.10.0` version of `@supabase/realtime-js`
* Check that you're using `@supabase/realtime-js` v2.44.0 or later.
* You need to define that the channel is private using the new configuration field during channel creation:
```typescript
let channel = supabase.realtime.channel(selectedRoom, {
config: { broadcast: { self: true }, private: true },
})
```
You can check `app/protected/page.tsx` for more information regarding this
```typescript
const channel = supabase.channel('room-1', {
config: { private: true },
})
```
You can check `app/protected/page.tsx` to see how we've set it up in the demo.
## Adding user to channel
Type `/invite <email>` of an existing user and they will be added to the channel.
![](invite.png)
![invite user slash command](invite.png)
@@ -23,15 +23,15 @@ export default function Chat() {
}
const addUserToChannel = async (email: string) => {
const user = await supabase.from('profiles').select('user_id').eq('email', email)
const user = await supabase.from('profiles').select('id').eq('email', email)
if (!user.data?.length) {
addMessage(true, true, `User ${email} not found`)
} else {
const room = await supabase.from('rooms').select('id').eq('topic', selectedRoom)
const room = await supabase.from('rooms').select('topic').eq('topic', selectedRoom)
await supabase
.from('rooms_users')
.upsert({ user_id: user.data?.[0].user_id, room_id: room.data?.[0].id })
.upsert({ user_id: user.data?.[0].id, room_topic: room.data?.[0].topic })
addMessage(true, true, `Added ${email} to channel ${selectedRoom}`)
}
}
@@ -65,7 +65,7 @@ export default function Chat() {
await supabase.auth.getUser()
const token = (await supabase.auth.getSession()).data.session?.access_token!
supabase.realtime.setAuth(token)
let main = supabase.realtime
let main = supabase
.channel('supaslack')
.on('broadcast', { event: 'new_room' }, () => getChannels())
.subscribe()
@@ -86,7 +86,7 @@ export default function Chat() {
channel?.unsubscribe()
setUsers(new Set())
let newChannel = supabase.realtime.channel(selectedRoom, {
let newChannel = supabase.channel(selectedRoom, {
config: {
broadcast: { self: true },
private: true, // This line will tell the server that you want to use a private channel for this connection
@@ -14,12 +14,12 @@ export default function CreateRoomModal({ channel }: { channel: RealtimeChannel
supabase.realtime.setAuth(token)
const rooms_response = await supabase.from('rooms').insert({ topic }).select('id')
const rooms_response = await supabase.from('rooms').insert({ topic }).select('topic')
if (rooms_response.data) {
await supabase
.from('rooms_users')
.insert({ user_id: user.data.user!.id, room_id: rooms_response.data![0].id })
.insert({ user_id: user.data.user!.id, room_topic: rooms_response.data![0].topic })
await channel?.send({
type: 'broadcast',
+14 -14
View File
@@ -62,7 +62,7 @@
"autoprefixer": "^10.4.14",
"nextjs-node-loader": "^1.1.5",
"postcss": "^8.4.31",
"typescript": "^5.4.3"
"typescript": "^5.5.0"
}
},
"apps/database-new/node_modules/@tanstack/query-core": {
@@ -149,7 +149,7 @@
"shiki": "^1.1.7",
"tailwindcss": "^3.3.0",
"tsconfig": "*",
"typescript": "^5",
"typescript": "^5.5.0",
"unist-builder": "3.0.0"
}
},
@@ -1011,7 +1011,7 @@
"simple-git": "^3.24.0",
"tsconfig": "*",
"tsx": "^4.6.2",
"typescript": "^5.4.3"
"typescript": "^5.5.0"
}
},
"apps/docs/node_modules/@tanstack/query-core": {
@@ -1363,7 +1363,7 @@
"sass": "^1.77.6",
"storybook-dark-mode": "^3.0.1",
"tailwindcss": "^3.4.1",
"typescript": "^5.4.3",
"typescript": "^5.5.0",
"vite": "^5.3.2",
"vite-plugin-cjs-interop": "^2.1.1"
}
@@ -2373,7 +2373,7 @@
"swiper": "^11.0.7",
"three": "^0.136.0",
"typed.js": "^2.0.16",
"typescript": "^5.4.3",
"typescript": "^5.5.0",
"ui": "*",
"ui-patterns": "*",
"use-debounce": "^7.0.1",
@@ -42899,9 +42899,9 @@
"license": "MIT"
},
"node_modules/typescript": {
"version": "5.4.3",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.4.3.tgz",
"integrity": "sha512-KrPd3PKaCLr78MalgiwJnA25Nm8HAmdwN3mYUYZgG/wizIo9EainNVQI9/yDavtVFRN2h3k8uf3GLHuhDMgEHg==",
"version": "5.5.2",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.5.2.tgz",
"integrity": "sha512-NcRtPEOsPFFWjobJEtfihkLCZCXZt/os3zf8nTxjVH3RvTSxjrCamJpbExGvYOF+tFHc3pA65qpdwPbzjohhew==",
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"
@@ -45475,7 +45475,7 @@
"sql-formatter": "^15.0.2",
"ts-jest": "^29.1.1",
"tsconfig": "*",
"typescript": "^5.4.3"
"typescript": "^5.5.0"
}
},
"packages/ai-commands/node_modules/argparse": {
@@ -45584,7 +45584,7 @@
"@types/react": "^18.2.24",
"@types/react-dom": "^18.2.8",
"tsconfig": "*",
"typescript": "^5.4.3"
"typescript": "^5.5.0"
},
"peerDependencies": {
"@supabase/auth-js": "^2.63.0",
@@ -45623,7 +45623,7 @@
"packages/icons": {
"dependencies": {
"@supabase/build-icons": "*",
"typescript": "^5.4.3"
"typescript": "^5.5.0"
}
},
"packages/pg-meta": {
@@ -45636,7 +45636,7 @@
},
"devDependencies": {
"@types/pg-format": "^1.0.1",
"typescript": "^5.4.3"
"typescript": "^5.5.0"
}
},
"packages/shared-data": {
@@ -45750,7 +45750,7 @@
"ts-jest": "^29.1.1",
"tsconfig": "*",
"tsconfig-paths-webpack-plugin": "^4.0.1",
"typescript": "^5.4.3"
"typescript": "^5.5.0"
},
"peerDependencies": {
"next": "*",
@@ -46515,7 +46515,7 @@
"jest-environment-jsdom": "^29.7.0",
"postgres": "^3.0.5",
"ts-jest": "^29.1.1",
"typescript": "^5.4.3"
"typescript": "^5.5.0"
}
},
"tests/node_modules/@testdeck/core": {
+1 -1
View File
@@ -34,6 +34,6 @@
"sql-formatter": "^15.0.2",
"ts-jest": "^29.1.1",
"tsconfig": "*",
"typescript": "^5.4.3"
"typescript": "^5.5.0"
}
}
+1 -1
View File
@@ -20,7 +20,7 @@
"@types/react": "^18.2.24",
"@types/react-dom": "^18.2.8",
"tsconfig": "*",
"typescript": "^5.4.3"
"typescript": "^5.5.0"
},
"peerDependencies": {
"@supabase/supabase-js": "*",
+1 -1
View File
@@ -5,7 +5,7 @@
"build:icons": "build-icons --templateSrc=./scripts/exportTemplate.mjs --renderUniqueKey --iconFileExtension=.ts --exportFileName=index.ts"
},
"dependencies": {
"typescript": "^5.4.3",
"typescript": "^5.5.0",
"@supabase/build-icons": "*"
}
}
+1 -1
View File
@@ -11,6 +11,6 @@
},
"devDependencies": {
"@types/pg-format": "^1.0.1",
"typescript": "^5.4.3"
"typescript": "^5.5.0"
}
}
+1 -1
View File
@@ -116,7 +116,7 @@
"ts-jest": "^29.1.1",
"tsconfig": "*",
"tsconfig-paths-webpack-plugin": "^4.0.1",
"typescript": "^5.4.3"
"typescript": "^5.5.0"
},
"peerDependencies": {
"next": "*",
+1 -1
View File
@@ -31,6 +31,6 @@
"jest-allure2-adapter": "^0.3.12",
"jest-environment-jsdom": "^29.7.0",
"postgres": "^3.0.5",
"typescript": "^5.4.3"
"typescript": "^5.5.0"
}
}