mirror of
https://github.com/supabase/supabase.git
synced 2026-10-07 02:15:05 +03:00
32 files changed
+2510
-110
No files matched your search
@@ -165,7 +165,7 @@ export const menuItems: NavMenu = {
|
||||
{ name: 'Database Connections', url: '/guides/database/connecting-to-postgres', items: [] },
|
||||
{ name: 'Tables and Data', url: '/guides/database/tables', items: [] },
|
||||
{ name: 'Database Functions', url: '/guides/database/functions', items: [] },
|
||||
{ name: 'Database Webhooks', url: '/guides/database/database-webhooks', items: [] },
|
||||
{ name: 'Database Webhooks', url: '/guides/database/webhooks', items: [] },
|
||||
{ name: 'Full Text Search', url: '/guides/database/full-text-search', items: [] },
|
||||
{ name: 'Database Testing', url: '/guides/database/testing', items: [] },
|
||||
{
|
||||
@@ -318,6 +318,7 @@ export const menuItems: NavMenu = {
|
||||
{ name: 'Directus', url: '/guides/integrations/directus', items: [] },
|
||||
{ name: 'Draftbit', url: '/guides/integrations/draftbit', items: [] },
|
||||
{ name: 'Plasmic', url: '/guides/integrations/plasmic', items: [] },
|
||||
{ name: 'WeWeb', url: '/guides/integrations/weweb', items: [] },
|
||||
],
|
||||
},
|
||||
],
|
||||
|
||||
File renamed without changes.
@@ -0,0 +1,384 @@
|
||||
---
|
||||
id: weweb
|
||||
title: 'WeWeb'
|
||||
description: 'Build user interfaces on top of existing databases.'
|
||||
---
|
||||
|
||||
This guide explains how to connect a Supabase back-end to a WeWeb front-end and then configure all the CRUD operations necessary to build an Admin Portal with user authentication, roles, and permissions.
|
||||
|
||||
[WeWeb](https://dashboard.weweb.io/sign-up) is a low-code front-end builder that allies the short learning curve of no-code with the freedom of code.
|
||||
|
||||
It connects to Supabase via two native integrations:
|
||||
- one for data manipulation, and
|
||||
- another for user authentication.
|
||||
|
||||
If you don’t have an WeWeb account, you can create one [here](https://dashboard.weweb.io/sign-up).
|
||||
|
||||
Let’s get started!
|
||||
|
||||
|
||||
## Step 1: Add the Supabase Data Source Plugin in WeWeb
|
||||
|
||||
In order to read Supabase data in WeWeb, you'll first need to add the Supabase Data Source Plugin:
|
||||
|
||||

|
||||
|
||||
|
||||
Once you've added it, you will be invited to share your Supabase project URL and public API key:
|
||||
|
||||

|
||||
|
||||
|
||||
In Supabase, you can find both your project URL and public key in the `Settings` > `API` menu:
|
||||
|
||||

|
||||
|
||||
|
||||
Once you have added both to WeWeb, you will have the option to enable realtime tables if you wish to do so:
|
||||
|
||||

|
||||
|
||||
|
||||
**🚨 Warning 🚨**
|
||||
|
||||
> Realtime is disabled by default in Supabase for better database performance and security. Learn more about [realtime functionalities](https://supabase.com/docs/guides/database#realtime) in our documentation.
|
||||
Member
|
||||
@awalias awalias 16 days ago
|
||||
might just wanna change these ones to be relative but otherwise looks great 👍
|
||||
|
||||
@EWMAs Reply...
|
||||
|
||||
|
||||
|
||||
## Step 2: GET Data from Supabase
|
||||
|
||||
Once you click on `Add a Collection`, you will be invited to give your Collection a name and choose Supabase as a Data source:
|
||||
|
||||

|
||||
|
||||
|
||||
You will then be able to select the Table from which you want to pull data:
|
||||
|
||||

|
||||
|
||||
|
||||
|
||||
Notice that this gives you access to 2 separate modes to access the fields in the table:
|
||||
|
||||
1. a "Guided" mode, and
|
||||
2. an "Advanced" mode.
|
||||
|
||||
|
||||
### Guided mode
|
||||
|
||||
By default, the "Guided" mode returns the data from all the fields.
|
||||
|
||||
In the example below, we decide to exclude the data from the `created_at` field in our `vehicles` table:
|
||||
|
||||
__
|
||||
|
||||
|
||||
As a result, WeWeb does not fetch the `created_at` field.
|
||||
|
||||
This is helpful because we can exclude data that we don't want to load in the frontend, either because we don't need it or because it's confidential.
|
||||
|
||||
|
||||
### Advanced mode
|
||||
|
||||
In our database, we created 2 separate tables for vehicles and locations.
|
||||
|
||||
In the `vehicles` table, we made a reference to the `locations` table in our `location_id` field so we know where each car is:
|
||||
|
||||
__
|
||||
|
||||
The problem is, the link only gives us the id of the location in the `locations` table.
|
||||
|
||||
If you choose the "Advanced" mode, you will be able to get the `name` field of the location instead of the `id`.
|
||||
|
||||
How?
|
||||
|
||||
By [making custom queries to Supabase](https://supabase.com/docs/reference/javascript/select):
|
||||
|
||||
__
|
||||
|
||||
|
||||
In the example above, we are telling Supabase:
|
||||
|
||||
- from the table selected in the Collection – in this case the `vehicles` table – please send me the data in the `id`, `model`, and `mileage` fields
|
||||
- look for the `location_id` in the `vehicles` table in the `locations` table and send me the data in the corresponding `name` field
|
||||
|
||||
If we only ask for the data from the `location` field of the `vehicles` table, Supabase will only return the `id`:
|
||||
|
||||

|
||||
|
||||
|
||||
**🚨 Warning 🚨**
|
||||
|
||||
> If you have enabled Row-Level Security in Supabase, make sure you have also [added a Policy](https://supabase.com/docs/learn/auth-deep-dive/auth-policies) that allows users to read the data in the table. Otherwise, WeWeb won't be able to get the data.
|
||||
|
||||
|
||||
|
||||
## Step 3: Display Supabase Data in WeWeb
|
||||
|
||||
Assuming you were able to fetch data from Supabase in a WeWeb Collection, you'll be able to bind the data from that Collection on your WeWeb pages.
|
||||
|
||||
In the example below, we chose to display the car model and mileage in the [Data Grid element](https://docs.weweb.io/data-grid/) that comes out-of-the-box in WeWeb:
|
||||
|
||||

|
||||
|
||||
We chose this element because it includes a built-in inline editing mode we'll want to use later for our CRUD operations.
|
||||
|
||||
|
||||
#### 🔥 Pro Tip 🔥
|
||||
|
||||
> In WeWeb, you can [bind arrays of data to any Container](https://docs.weweb.io/display-collection-list/). Just bear in mind that the first child of the Container you bind the Collection to will be the repeated item. With that in mind, you might want the first child Element to be another Container with a number of items inside like a title, description, button or image.
|
||||
|
||||
|
||||
|
||||
## Step 4: Update a record in Supabase
|
||||
|
||||
Once you've added a Supabase Collection of data to WeWeb, you might want to allow users to manipulate the data in that Collection.
|
||||
|
||||
In order to do so, you'll need to create a Workflow in WeWeb.
|
||||
|
||||
In the example below, we are using the "Update row" Workflow that comes by default with WeWeb's Data Grid Element.
|
||||
|
||||
The trigger is `On Row update`.
|
||||
|
||||
Since we added the Supabase Data Source Plugin above, we have access to all the CRUD actions available in Supabase:
|
||||
- Select
|
||||
- Insert
|
||||
- Update
|
||||
- Upsert
|
||||
- Delete
|
||||
|
||||
In this case, we choose the "Update" action:
|
||||
|
||||

|
||||
|
||||
|
||||
Then, in our "Update" action, we select the `vehicles` table and map the `id` to the id of the Workflow Event:
|
||||
|
||||

|
||||
|
||||
|
||||
Finally, we tell WeWeb we want to update the `mileage` field in our Supabase table, and send the value in the `mileage` column of our Data Grid:
|
||||
|
||||

|
||||
|
||||
And that's it!
|
||||
|
||||
If you switch to Preview mode, you will be update your Supabase table from your WeWeb Data Grid:
|
||||
|
||||

|
||||
|
||||
|
||||
**🔥 Pro Tip 🔥**
|
||||
|
||||
> By default, the fields in the Data Grid Element are Text fields but you can change the input type to Number if you need to send numerical data to your database:
|
||||
|
||||

|
||||
|
||||
|
||||
## Restrict who can modify a record in Supabase
|
||||
|
||||
By default, all the data in the tables that are in the `public` schema of your Supabase project can be read, updated, or deleted.
|
||||
|
||||
Supabase allows you to [enable Row-Level Security](https://supabase.com/docs/learn/auth-deep-dive/auth-row-level-security) for each of your tables:
|
||||
|
||||

|
||||
|
||||
|
||||
If you want to restrict certain actions to specific users or roles, you'll need to:
|
||||
|
||||
- add Supabase authentication to your WeWeb project, and
|
||||
- [write SQL policies in Supabase](https://supabase.com/docs/learn/auth-deep-dive/auth-policies).
|
||||
|
||||
|
||||
We provide a number of policy templates to get you started:
|
||||
|
||||

|
||||
|
||||
|
||||
In the example below, we say that users can:
|
||||
|
||||
1. update a record
|
||||
2. in the "locations" table of the "public" schema
|
||||
3. if they are authenticated
|
||||
|
||||

|
||||
|
||||
|
||||
**🔥 Pro Tip 🔥**
|
||||
|
||||
> Once you enable RLS on a Supabase table, you won't be able to access the data in a WeWeb Collection unless you've added a policy.
|
||||
|
||||

|
||||
|
||||
|
||||
|
||||
|
||||
## Step 4: Add User Authentication
|
||||
Once you are able to display Supabase data in WeWeb, you might want to restrict access to certain users or display specific data based on a user's role.
|
||||
|
||||
In order to do that, you'll need to add WeWeb's Supabase Auth Plugin.
|
||||
|
||||
|
||||
### Add Supabase Auth Plugin in WeWeb
|
||||
|
||||
Supabase comes with an in-built authentication system which you can use in WeWeb.
|
||||
|
||||
To add the Supabase Auth Plugin in WeWeb, go to `Plugins` > `Authentication`:
|
||||
|
||||

|
||||
|
||||
|
||||
Assuming you have already provided your Supabase project URL and public API key when setting up the Supabase Data source, the only thing left to do will be to add your private API key:
|
||||
|
||||

|
||||
|
||||
|
||||
In Supabase, you can find your private API key in `Settings` > `API`:
|
||||
|
||||

|
||||
|
||||
|
||||
**🚨 Warning 🚨**
|
||||
|
||||
> As the name suggests, you'll want to keep this API key secret! Assuming you copy it properly in the "Private API key" field of the Supabase Auth Plugin and don't use it anywhere else in your Weweb project, Weweb will never make it public.
|
||||
|
||||
You will then be invited to choose a page to redirect _unauthenticated_ users, i.e. users who are NOT signed-in:
|
||||
|
||||

|
||||
|
||||
|
||||
**🚨 Warning 🚨**
|
||||
|
||||
> When you setup your Login Workflow, make sure you don't redirect unauthenticated users to a page that is only accessible to authenticated users. Otherwise, you'll be creating an **infinite loop** and your app will crash.
|
||||
|
||||
|
||||
### Create User Sign Up and Sign In Workflows
|
||||
|
||||
In the `Add` > `UI kit` menu of WeWeb, you can find ready-made Sign in and Sign up Forms:
|
||||
|
||||

|
||||
|
||||
|
||||
Once you've added a Form to the Canvas, you'll be able to style it whichever way you want.
|
||||
|
||||
In the example below, we added an image with the logo of our project to a Sign up Form and changed the background color of the `Create Form` Container:
|
||||
|
||||

|
||||
|
||||
|
||||
To allow users to sign up, you'll need to create a Sign up Workflow on the Form Container:
|
||||
|
||||

|
||||
|
||||
|
||||
**🔥 Pro Tip 🔥**
|
||||
|
||||
> It's important that the Workflow is on the Form Container and not the Sign up Button because we want to validate the fields of the Form when users submit it.
|
||||
|
||||
In the Workflow, you will choose the `On submit` trigger and add the Supabase `Sign up` Action:
|
||||
|
||||

|
||||
|
||||
|
||||
Then, you'll want to map the email, password, and metadata information in the Form to the email, password, and metadata in Supabase before choosing what page the new user should be redirected to:
|
||||
|
||||

|
||||
|
||||
|
||||
In the example above, we made sure to add the user's name as an item in that user's metadata.
|
||||
|
||||
In Supabase, you can find the user's metadata in JSON format in a dedicated field of the `users` table, named `raw_user_meta_data`:
|
||||
|
||||

|
||||
|
||||
The same logic will apply to any Supabase Action you want to trigger.
|
||||
|
||||
|
||||
## Adding User Roles & Permissions
|
||||
|
||||
Now let's say we want to gate content and set different permissions based on a user's role.
|
||||
|
||||
### Adding Roles in Supabase
|
||||
|
||||
In Supabase, we'll need to create a `roles` table with a list of roles and a join table that links the `roles` table with our `users` table.
|
||||
|
||||
First, let's create a `roles` table with three roles and make sure that each role had a UUID and a `name`:
|
||||
|
||||

|
||||
|
||||
|
||||
**🚨 Warning 🚨**
|
||||
|
||||
> In order for the integration to work with the Users tab in WeWeb, it is crucial that the role title is a text field named `name`.
|
||||
|
||||
|
||||
### Joining Roles and Users in Supabase
|
||||
|
||||
Second, let's create a `userRoles` join table:
|
||||
|
||||

|
||||
|
||||
|
||||
In the join table above, you can see we have an `id` field that is uniquely identifiable thanks to a UUID.
|
||||
|
||||
This unique `id` is linked to a `userId`, which is also a UUID, more specifically, it is the UUID we find in the `id` field of the `users` table in the `auth` schema:
|
||||
|
||||

|
||||
|
||||
|
||||
Each row in our `userRoles` table is also linked to a `roleId` which is the UUID we find in the `id` field of the `roles` table in the `public` schema:
|
||||
|
||||

|
||||
|
||||
|
||||
### Linking Users in WeWeb to Roles and Users in Supabase
|
||||
|
||||
Once we've added our list of roles in Supabase and created an empty join table to link our roles with users, it's time to go to WeWeb.
|
||||
|
||||
In `Plugins` > `Supabase Auth` > `3. Roles table`, we'll click `refresh` and select the relevant Supabase tables we just created:
|
||||
|
||||

|
||||
|
||||
|
||||
Once you've told WeWeb where to find the `roles` and the join table in Supabase, you'll be able to easily view and maintain user roles in the `Users` tab in WeWeb:
|
||||
|
||||

|
||||
|
||||
When you make a change to a User in WeWeb, it will automatically be updated in Supabase.
|
||||
|
||||
|
||||
## Users vs Profiles
|
||||
|
||||
So far, we've showed you how to work with the default `users` table that Supabase generates in the `auth` schema when you create a new project.
|
||||
|
||||
Note that, for security purposes, the information in that `users` table is not exposed on the auto-generated API.
|
||||
|
||||
How does this affect your project in WeWeb?
|
||||
|
||||
### Let users update their information
|
||||
|
||||
Let's say you want to let authenticated users update their information, then you don't need to set up anything else in Supabase.
|
||||
|
||||
You could simply create a user profile page in WeWeb and display their information when they sign in, based on the data you have in the `user` Variable:
|
||||
|
||||

|
||||
|
||||
|
||||
### Display other users' information
|
||||
|
||||
In some use cases, you might want to display _other_ users' information.
|
||||
|
||||
For example, if you're building an HR portal in WeWeb, you might want HR employees to have access to a list of applicants and their user profiles.
|
||||
|
||||
You wouldn't be able to do that with the `users` table in the `auth` schema because each user's information is only available to them.
|
||||
|
||||
For such a use case, we recommend creating a `profiles` table in the `public` schema to store user data that you want to access via the API.
|
||||
|
||||
In WeWeb, you would then be able to create a Collection to get data from the `profiles` table.
|
||||
|
||||
Learn more about [managing user data in Supabase](https://supabase.com/docs/guides/auth/managing-user-data).
|
||||
@@ -93,9 +93,9 @@ const { data, error } = await supabase.storage.createBucket('avatars')
|
||||
|
||||
```dart
|
||||
void main() async {
|
||||
final client = SupabaseClient('supabaseUrl', 'supabaseKey');
|
||||
final supabase = SupabaseClient('supabaseUrl', 'supabaseKey');
|
||||
|
||||
final storageResponse = await client
|
||||
final storageResponse = await supabase
|
||||
.storage
|
||||
.createBucket('avatars');
|
||||
}
|
||||
@@ -140,12 +140,12 @@ const { data, error } = await supabase.storage
|
||||
|
||||
```dart
|
||||
void main() async {
|
||||
final client = SupabaseClient('supabaseUrl', 'supabaseKey');
|
||||
final supabase = SupabaseClient('supabaseUrl', 'supabaseKey');
|
||||
|
||||
// Create file `example.txt` and upload it in `public` bucket
|
||||
final file = File('example.txt');
|
||||
file.writeAsStringSync('File content');
|
||||
final storageResponse = await client
|
||||
final storageResponse = await supabase
|
||||
.storage
|
||||
.from('public')
|
||||
.upload('example.txt', file);
|
||||
@@ -190,9 +190,9 @@ const { data, error } = await supabase.storage.from('avatars').download('public/
|
||||
|
||||
```dart
|
||||
void main() async {
|
||||
final client = SupabaseClient('supabaseUrl', 'supabaseKey');
|
||||
final supabase = SupabaseClient('supabaseUrl', 'supabaseKey');
|
||||
|
||||
final storageResponse = await client
|
||||
final storageResponse = await supabase
|
||||
.storage
|
||||
.from('public')
|
||||
.download('example.txt');
|
||||
|
||||
@@ -526,3 +526,7 @@ At this stage you have a fully functional application!
|
||||
|
||||
- Got a question? [Ask here](https://github.com/supabase/supabase/discussions).
|
||||
- Sign in: [app.supabase.com](https://app.supabase.com)
|
||||
|
||||
## Resources
|
||||
|
||||
- [Authentication in Ionic Angular with Supabase](https://supabase.com/blog/authentication-in-ionic-angular)
|
||||
@@ -110,11 +110,11 @@ import { createBrowserSupabaseClient } from '@supabase/auth-helpers-nextjs'
|
||||
import { SessionContextProvider } from '@supabase/auth-helpers-react'
|
||||
|
||||
function MyApp({ Component, pageProps }) {
|
||||
const [supabaseClient] = useState(() => createBrowserSupabaseClient())
|
||||
const [supabase] = useState(() => createBrowserSupabaseClient())
|
||||
|
||||
return (
|
||||
<SessionContextProvider
|
||||
supabaseClient={supabaseClient}
|
||||
supabaseClient={supabase}
|
||||
initialSession={pageProps.initialSession}
|
||||
>
|
||||
<Component {...pageProps} />
|
||||
@@ -139,11 +139,11 @@ function MyApp({
|
||||
}: AppProps<{
|
||||
initialSession: Session,
|
||||
}>) {
|
||||
const [supabaseClient] = useState(() => createBrowserSupabaseClient())
|
||||
const [supabase] = useState(() => createBrowserSupabaseClient())
|
||||
|
||||
return (
|
||||
<SessionContextProvider
|
||||
supabaseClient={supabaseClient}
|
||||
supabaseClient={supabase}
|
||||
initialSession={pageProps.initialSession}
|
||||
>
|
||||
<Component {...pageProps} />
|
||||
|
||||
@@ -305,12 +305,12 @@ import { createClient } from '@supabase/supabase-js'
|
||||
|
||||
// ...
|
||||
|
||||
const supabaseClient = createClient(process.env.SUPABASE_URL, process.env.SUPABASE_KEY)
|
||||
const supabase = createClient(process.env.SUPABASE_URL, process.env.SUPABASE_KEY)
|
||||
|
||||
const App = () => (
|
||||
<FatalErrorBoundary page={FatalErrorPage}>
|
||||
<RedwoodProvider titleTemplate="%PageTitle | %AppTitle">
|
||||
<AuthProvider client={supabaseClient} type="supabase">
|
||||
<AuthProvider client={supabase} type="supabase">
|
||||
<RedwoodApolloProvider>
|
||||
<Routes />
|
||||
</RedwoodApolloProvider>
|
||||
|
||||
@@ -56,7 +56,7 @@ on the browser, and that's completely fine since we have [Row Level Security](/d
|
||||
import { createClient } from '@supabase/auth-helpers-sveltekit'
|
||||
import { env } from '$env/dynamic/public'
|
||||
|
||||
export const supabaseClient = createClient(env.PUBLIC_SUPABASE_URL, env.PUBLIC_SUPABASE_ANON_KEY)
|
||||
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.
|
||||
@@ -78,7 +78,7 @@ Update your `src/routes/+layout.svelte`:
|
||||
|
||||
```html title=src/routes/+layout.svelte
|
||||
<script lang="ts">
|
||||
import { supabaseClient } from '$lib/supabaseClient'
|
||||
import { supabase } from '$lib/supabaseClient'
|
||||
import { invalidate } from '$app/navigation'
|
||||
import { onMount } from 'svelte'
|
||||
import './styles.css'
|
||||
@@ -86,7 +86,7 @@ Update your `src/routes/+layout.svelte`:
|
||||
onMount(() => {
|
||||
const {
|
||||
data: { subscription },
|
||||
} = supabaseClient.auth.onAuthStateChange(() => {
|
||||
} = supabase.auth.onAuthStateChange(() => {
|
||||
invalidate('supabase:auth')
|
||||
})
|
||||
|
||||
@@ -142,7 +142,7 @@ Let's set up a Svelte component to manage logins and sign ups. We'll use Magic L
|
||||
|
||||
```html title=src/routes/Auth.svelte
|
||||
<script lang="ts">
|
||||
import { supabaseClient } from '$lib/supabaseClient'
|
||||
import { supabase } from '$lib/supabaseClient'
|
||||
|
||||
let loading = false
|
||||
let email: string
|
||||
@@ -150,7 +150,7 @@ Let's set up a Svelte component to manage logins and sign ups. We'll use Magic L
|
||||
const handleLogin = async () => {
|
||||
try {
|
||||
loading = true
|
||||
const { error } = await supabaseClient.auth.signInWithOtp({ email })
|
||||
const { error } = await supabase.auth.signInWithOtp({ email })
|
||||
if (error) throw error
|
||||
alert('Check your email for the login link!')
|
||||
} catch (error) {
|
||||
@@ -187,7 +187,7 @@ Create a new `Account.svelte` component to handle this functionality.
|
||||
<script lang="ts">
|
||||
import { onMount } from 'svelte'
|
||||
import type { AuthSession } from '@supabase/supabase-js'
|
||||
import { supabaseClient } from '$lib/supabaseClient'
|
||||
import { supabase } from '$lib/supabaseClient'
|
||||
|
||||
export let session: AuthSession
|
||||
|
||||
@@ -205,7 +205,7 @@ Create a new `Account.svelte` component to handle this functionality.
|
||||
loading = true
|
||||
const { user } = session
|
||||
|
||||
const { data, error, status } = await supabaseClient
|
||||
const { data, error, status } = await supabase
|
||||
.from('profiles')
|
||||
.select(`username, website, avatar_url`)
|
||||
.eq('id', user.id)
|
||||
@@ -240,7 +240,7 @@ Create a new `Account.svelte` component to handle this functionality.
|
||||
updated_at: new Date(),
|
||||
}
|
||||
|
||||
let { error } = await supabaseClient.from('profiles').upsert(updates)
|
||||
let { error } = await supabase.from('profiles').upsert(updates)
|
||||
|
||||
if (error) throw error
|
||||
} catch (error) {
|
||||
@@ -255,7 +255,7 @@ Create a new `Account.svelte` component to handle this functionality.
|
||||
async function signOut() {
|
||||
try {
|
||||
loading = true
|
||||
let { error } = await supabaseClient.auth.signOut()
|
||||
let { error } = await supabase.auth.signOut()
|
||||
if (error) throw error
|
||||
} catch (error) {
|
||||
if (error instanceof Error) {
|
||||
@@ -336,7 +336,7 @@ Let's create an avatar for the user so that they can upload a profile photo. We
|
||||
```html title=src/routes/Avatar.svelte
|
||||
<script lang="ts">
|
||||
import { createEventDispatcher } from 'svelte'
|
||||
import { supabaseClient } from '$lib/supabaseClient'
|
||||
import { supabase } from '$lib/supabaseClient'
|
||||
|
||||
export let size = 10
|
||||
export let url: string
|
||||
@@ -349,7 +349,7 @@ Let's create an avatar for the user so that they can upload a profile photo. We
|
||||
|
||||
const downloadImage = async (path: string) => {
|
||||
try {
|
||||
const { data, error } = await supabaseClient.storage.from('avatars').download(path)
|
||||
const { data, error } = await supabase.storage.from('avatars').download(path)
|
||||
|
||||
if (error) {
|
||||
throw error
|
||||
@@ -376,7 +376,7 @@ Let's create an avatar for the user so that they can upload a profile photo. We
|
||||
const fileExt = file.name.split('.').pop()
|
||||
const filePath = `${Math.random()}.${fileExt}`
|
||||
|
||||
let { error } = await supabaseClient.storage.from('avatars').upload(filePath, file)
|
||||
let { error } = await supabase.storage.from('avatars').upload(filePath, file)
|
||||
|
||||
if (error) {
|
||||
throw error
|
||||
|
||||
@@ -123,14 +123,14 @@ const { data } = await supabase.auth.signIn({
|
||||
There is a new `channel()` method in the Realtime library, which will be used for our Multiplayer updates.
|
||||
|
||||
```ts
|
||||
supabaseClient
|
||||
supabase
|
||||
.channel('any_string_you_want')
|
||||
.on('presence', { event: 'track' }, (payload) => {
|
||||
console.log(payload)
|
||||
})
|
||||
.subscribe()
|
||||
|
||||
supabaseClient
|
||||
supabase
|
||||
.channel('any_string_you_want')
|
||||
.on(
|
||||
'postgres_changes',
|
||||
|
||||
@@ -28,7 +28,7 @@ _Optionally_ if you are using custom configuration with `createClient` then foll
|
||||
<TabPanel id="1.0x" label="Before">
|
||||
|
||||
```ts title=src/supabaseClient.ts
|
||||
const supabaseClient = createClient(SUPABASE_URL, SUPABASE_ANON_KEY, {
|
||||
const supabase = createClient(SUPABASE_URL, SUPABASE_ANON_KEY, {
|
||||
schema: 'custom',
|
||||
persistSession: false,
|
||||
})
|
||||
@@ -38,7 +38,7 @@ const supabaseClient = createClient(SUPABASE_URL, SUPABASE_ANON_KEY, {
|
||||
<TabPanel id="2.0x" label="After">
|
||||
|
||||
```ts title=src/supabaseClient.ts
|
||||
const supabaseClient = createClient(SUPABASE_URL, SUPABASE_ANON_KEY, {
|
||||
const supabase = createClient(SUPABASE_URL, SUPABASE_ANON_KEY, {
|
||||
db: {
|
||||
schema: 'custom',
|
||||
},
|
||||
|
||||
@@ -0,0 +1,85 @@
|
||||
import { writeFileSync } from 'fs'
|
||||
import { globby } from 'globby'
|
||||
import prettier from 'prettier'
|
||||
|
||||
/*
|
||||
* kudos to leerob from vercel
|
||||
* https://leerob.io/blog/nextjs-sitemap-robots
|
||||
*/
|
||||
|
||||
async function generate() {
|
||||
const prettierConfig = await prettier.resolveConfig('./.prettierrc.js')
|
||||
|
||||
const guidesPages = await globby(['docs/*.mdx', 'docs/guides/**/*.mdx', 'docs/handbook/**/*.mdx'])
|
||||
|
||||
const rawReferencePages = await globby([
|
||||
// guides
|
||||
'docs/*.mdx',
|
||||
'!docs/404.mdx',
|
||||
'docs/guides/**/*.mdx',
|
||||
'docs/handbook/**/*.mdx',
|
||||
// reference
|
||||
'docs/reference/*.mdx',
|
||||
'docs/reference/javascript/*.mdx',
|
||||
'docs/reference/javascript/generated/*.mdx',
|
||||
'!docs/reference/javascript/v1.mdx', // ignore this
|
||||
'docs/reference/dart/*.mdx',
|
||||
'docs/reference/dart/generated/*.mdx',
|
||||
'!docs/reference/dart/v0.mdx', // ignore this
|
||||
'docs/reference/api/*.mdx',
|
||||
'docs/reference/api/generated/*.mdx',
|
||||
'docs/reference/cli/*.mdx',
|
||||
'docs/reference/cli/generated/*.mdx',
|
||||
// misc reference
|
||||
'docs/reference/postgres/*.mdx',
|
||||
'docs/reference/postgres/generated/*.mdx',
|
||||
'docs/reference/realtime/*.mdx',
|
||||
'docs/reference/realtime/generated/*.mdx',
|
||||
'docs/reference/storage/*.mdx',
|
||||
'docs/reference/storage/generated/*.mdx',
|
||||
'docs/reference/auth/*.mdx',
|
||||
'docs/reference/auth/generated/*.mdx',
|
||||
])
|
||||
|
||||
const pages = rawReferencePages.map((x) => {
|
||||
let string = x
|
||||
string = string.replace('/generated', '')
|
||||
string = string.replace('.mdx', '')
|
||||
return string
|
||||
})
|
||||
|
||||
// add static OSS page
|
||||
pages.unshift('docs/oss')
|
||||
// add static homepage
|
||||
pages.unshift('docs')
|
||||
|
||||
const sitemap = `
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
|
||||
${pages
|
||||
.map((path) => {
|
||||
return `
|
||||
<url>
|
||||
<loc>${`https://supabase.com/${path}`}</loc>
|
||||
<changefreq>weekly</changefreq>
|
||||
<changefreq>0.5</changefreq>
|
||||
</url>
|
||||
`
|
||||
})
|
||||
.join('')}
|
||||
</urlset>
|
||||
`
|
||||
|
||||
const formatted = prettier.format(sitemap, {
|
||||
...prettierConfig,
|
||||
parser: 'html',
|
||||
})
|
||||
|
||||
const sitemapFilePath = `public/sitemap.xml`
|
||||
console.log(`Total of ${pages.length} pages in sitemap, located at /apps/docs/${sitemapFilePath}`)
|
||||
|
||||
// eslint-disable-next-line no-sync
|
||||
writeFileSync(sitemapFilePath, formatted)
|
||||
}
|
||||
|
||||
generate()
|
||||
@@ -7,8 +7,9 @@
|
||||
"build": "npm run generate:all && next build",
|
||||
"start": "next start",
|
||||
"lint": "next lint",
|
||||
"postbuild": "node ./scripts/build-search.js",
|
||||
"generate:all": "npm-run-all --parallel gen:api gen:cli gen:gotrue gen:storage gen:supabase-dart:v0 gen:supabase-dart:v1 gen:supabase-js:v1 gen:supabase-js:v2 gen:realtime",
|
||||
"build:sitemap": "node ./internals/generate-sitemap.mjs",
|
||||
"postbuild": "node ./scripts/build-search.js && node ./internals/generate-sitemap.mjs",
|
||||
"generate:all": "npm-run-all --parallel gen:api gen:cli gen:gotrue gen:storage gen:supabase-dart:v0 gen:supabase-dart:v1 gen:supabase-js:v1 gen:supabase-js:v2",
|
||||
"gen:api": "npm-run-all gen:api:usage",
|
||||
"gen:api:usage": "ts-node ./generator/index.ts gen --type api --url https://api.supabase.com --input ../../spec/transforms/api_v0_openapi_deparsed.json --output ./docs/reference/api/generated/usage.mdx",
|
||||
"gen:cli": "npm-run-all gen:cli:commands gen:cli:config",
|
||||
@@ -27,9 +28,7 @@
|
||||
"gen:supabase-js:v1": "npm-run-all gen:supabase-js:v1:ref",
|
||||
"gen:supabase-js:v1:ref": "ts-node ./generator/index.ts gen --type legacy --input ../../spec/supabase_js_v1.yml --output ./docs/reference/javascript/v1/generated",
|
||||
"gen:supabase-js:v2": "npm-run-all gen:supabase-js:v2:ref",
|
||||
"gen:supabase-js:v2:ref": "ts-node ./generator/index.ts gen --type legacy --input ../../spec/supabase_js_v2_temp.yml --output ./docs/reference/javascript/generated",
|
||||
"gen:realtime": "npm-run-all gen:realtime:config",
|
||||
"gen:realtime:config": "ts-node ./generator/index.ts gen --type config --input ../../spec/realtime_v0_config.yaml --output ./docs/reference/realtime/generated/config.mdx"
|
||||
"gen:supabase-js:v2:ref": "ts-node ./generator/index.ts gen --type legacy --input ../../spec/supabase_js_v2_temp.yml --output ./docs/reference/javascript/generated"
|
||||
},
|
||||
"dependencies": {
|
||||
"@algolia/autocomplete-js": "^1.7.2",
|
||||
@@ -83,6 +82,8 @@
|
||||
"sass": "^1.55.0",
|
||||
"ts-node": "^10.9.1",
|
||||
"tsconfig": "*",
|
||||
"typescript": "^4.5.3"
|
||||
"typescript": "^4.5.3",
|
||||
"globby": "^12.0.2",
|
||||
"prettier": "^2.2.1"
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large.
Load diff
@@ -126,14 +126,14 @@ const { data } = await supabase.auth.signIn({
|
||||
There is a new `channel()` method in the Realtime library, which will be used for our Multiplayer updates.
|
||||
|
||||
```ts
|
||||
supabaseClient
|
||||
supabase
|
||||
.channel('any_string_you_want')
|
||||
.on('presence', { event: 'track' }, (payload) => {
|
||||
console.log(payload)
|
||||
})
|
||||
.subscribe()
|
||||
|
||||
supabaseClient
|
||||
supabase
|
||||
.channel('any_string_you_want')
|
||||
.on(
|
||||
'postgres_changes',
|
||||
|
||||
@@ -30,7 +30,7 @@ npm install @supabase/supabase-js@2
|
||||
<TabItem value="1.x">
|
||||
|
||||
```ts title="src/supabaseClient.ts"
|
||||
const supabaseClient = createClient(SUPABASE_URL, SUPABASE_ANON_KEY, {
|
||||
const supabase = createClient(SUPABASE_URL, SUPABASE_ANON_KEY, {
|
||||
schema: 'custom',
|
||||
persistSession: false
|
||||
})
|
||||
@@ -40,7 +40,7 @@ const supabaseClient = createClient(SUPABASE_URL, SUPABASE_ANON_KEY, {
|
||||
<TabItem value="2.x">
|
||||
|
||||
```ts title="src/supabaseClient.ts"
|
||||
const supabaseClient = createClient(SUPABASE_URL, SUPABASE_ANON_KEY, {
|
||||
const supabase = createClient(SUPABASE_URL, SUPABASE_ANON_KEY, {
|
||||
db: {
|
||||
schema: 'custom'
|
||||
},
|
||||
|
||||
@@ -24,7 +24,7 @@ import { createClient } from 'https://esm.sh/@supabase/supabase-js@2.0.0-rc.12'
|
||||
serve(async (req: Request) => {
|
||||
try {
|
||||
// Create a Supabase client with the Auth context of the logged in user.
|
||||
const supabaseClient = createClient(
|
||||
const supabase = createClient(
|
||||
// Supabase API URL - env var exported by default.
|
||||
Deno.env.get('SUPABASE_URL') ?? '',
|
||||
// Supabase API ANON KEY - env var exported by default.
|
||||
@@ -38,12 +38,12 @@ serve(async (req: Request) => {
|
||||
// highlight-start
|
||||
const {
|
||||
data: { user },
|
||||
} = await supabaseClient.auth.getUser()
|
||||
} = await supabase.auth.getUser()
|
||||
// highlight-end
|
||||
|
||||
// And we can run queries in the context of our authenticated user
|
||||
// highlight-start
|
||||
const { data, error } = await supabaseClient.from('users').select('*')
|
||||
const { data, error } = await supabase.from('users').select('*')
|
||||
if (error) throw error
|
||||
// highlight-end
|
||||
|
||||
|
||||
@@ -94,9 +94,9 @@ const { data, error } = await supabase.storage.createBucket('avatars')
|
||||
|
||||
```dart
|
||||
void main() async {
|
||||
final client = SupabaseClient('supabaseUrl', 'supabaseKey');
|
||||
final supabase = SupabaseClient('supabaseUrl', 'supabaseKey');
|
||||
|
||||
final storageResponse = await client
|
||||
final storageResponse = await supabase
|
||||
.storage
|
||||
.createBucket('avatars');
|
||||
}
|
||||
@@ -143,12 +143,12 @@ const { data, error } = await supabase.storage
|
||||
|
||||
```dart
|
||||
void main() async {
|
||||
final client = SupabaseClient('supabaseUrl', 'supabaseKey');
|
||||
final supabase = SupabaseClient('supabaseUrl', 'supabaseKey');
|
||||
|
||||
// Create file `example.txt` and upload it in `public` bucket
|
||||
final file = File('example.txt');
|
||||
file.writeAsStringSync('File content');
|
||||
final storageResponse = await client
|
||||
final storageResponse = await supabase
|
||||
.storage
|
||||
.from('public')
|
||||
.upload('example.txt', file);
|
||||
@@ -195,9 +195,9 @@ const { data, error } = await supabase.storage
|
||||
|
||||
```dart
|
||||
void main() async {
|
||||
final client = SupabaseClient('supabaseUrl', 'supabaseKey');
|
||||
final supabase = SupabaseClient('supabaseUrl', 'supabaseKey');
|
||||
|
||||
final storageResponse = await client
|
||||
final storageResponse = await supabase
|
||||
.storage
|
||||
.from('public')
|
||||
.download('example.txt');
|
||||
|
||||
@@ -113,11 +113,11 @@ import { createBrowserSupabaseClient } from '@supabase/auth-helpers-nextjs'
|
||||
import { SessionContextProvider } from '@supabase/auth-helpers-react'
|
||||
|
||||
function MyApp({ Component, pageProps }) {
|
||||
const [supabaseClient] = useState(() => createBrowserSupabaseClient())
|
||||
const [supabase] = useState(() => createBrowserSupabaseClient())
|
||||
|
||||
return (
|
||||
<SessionContextProvider
|
||||
supabaseClient={supabaseClient}
|
||||
supabaseClient={supabase}
|
||||
initialSession={pageProps.initialSession}
|
||||
>
|
||||
<Component {...pageProps} />
|
||||
@@ -144,11 +144,11 @@ function MyApp({
|
||||
// highlight-next-line
|
||||
initialSession: Session,
|
||||
}>) {
|
||||
const [supabaseClient] = useState(() => createBrowserSupabaseClient())
|
||||
const [supabase] = useState(() => createBrowserSupabaseClient())
|
||||
|
||||
return (
|
||||
<SessionContextProvider
|
||||
supabaseClient={supabaseClient}
|
||||
supabaseClient={supabase}
|
||||
initialSession={pageProps.initialSession}
|
||||
>
|
||||
<Component {...pageProps} />
|
||||
|
||||
@@ -303,7 +303,7 @@ import { createClient } from '@supabase/supabase-js'
|
||||
|
||||
// ...
|
||||
|
||||
const supabaseClient = createClient(
|
||||
const supabase = createClient(
|
||||
process.env.SUPABASE_URL,
|
||||
process.env.SUPABASE_KEY
|
||||
)
|
||||
@@ -311,7 +311,7 @@ const supabaseClient = createClient(
|
||||
const App = () => (
|
||||
<FatalErrorBoundary page={FatalErrorPage}>
|
||||
<RedwoodProvider titleTemplate="%PageTitle | %AppTitle">
|
||||
<AuthProvider client={supabaseClient} type="supabase">
|
||||
<AuthProvider client={supabase} type="supabase">
|
||||
<RedwoodApolloProvider>
|
||||
<Routes />
|
||||
</RedwoodApolloProvider>
|
||||
|
||||
@@ -56,7 +56,7 @@ on the browser, and that's completely fine since we have [Row Level Security](/d
|
||||
import { createClient } from '@supabase/auth-helpers-sveltekit'
|
||||
import { env } from '$env/dynamic/public'
|
||||
|
||||
export const supabaseClient = createClient(env.PUBLIC_SUPABASE_URL, env.PUBLIC_SUPABASE_ANON_KEY)
|
||||
export const supabase = createClient(env.PUBLIC_SUPABASE_URL, env.PUBLIC_SUPABASE_ANON_KEY)
|
||||
```
|
||||
|
||||
And one optional step is to add the CSS file at `src/routes/styles.css` to make the app look nice.
|
||||
@@ -78,7 +78,7 @@ Update your `src/routes/+layout.svelte`:
|
||||
|
||||
```html title="src/routes/+layout.svelte"
|
||||
<script lang="ts">
|
||||
import { supabaseClient } from '$lib/supabaseClient'
|
||||
import { supabase } from '$lib/supabaseClient'
|
||||
import { invalidate } from '$app/navigation'
|
||||
import { onMount } from 'svelte'
|
||||
import './styles.css'
|
||||
@@ -86,7 +86,7 @@ Update your `src/routes/+layout.svelte`:
|
||||
onMount(() => {
|
||||
const {
|
||||
data: { subscription }
|
||||
} = supabaseClient.auth.onAuthStateChange(() => {
|
||||
} = supabase.auth.onAuthStateChange(() => {
|
||||
invalidate('supabase:auth')
|
||||
})
|
||||
|
||||
@@ -142,7 +142,7 @@ Let's set up a Svelte component to manage logins and sign ups. We'll use Magic L
|
||||
|
||||
```html title="src/routes/Auth.svelte"
|
||||
<script lang="ts">
|
||||
import { supabaseClient } from '$lib/supabaseClient'
|
||||
import { supabase } from '$lib/supabaseClient'
|
||||
|
||||
let loading = false
|
||||
let email: string
|
||||
@@ -150,7 +150,7 @@ Let's set up a Svelte component to manage logins and sign ups. We'll use Magic L
|
||||
const handleLogin = async () => {
|
||||
try {
|
||||
loading = true
|
||||
const { error } = await supabaseClient.auth.signInWithOtp({ email })
|
||||
const { error } = await supabase.auth.signInWithOtp({ email })
|
||||
if (error) throw error
|
||||
alert('Check your email for the login link!')
|
||||
} catch (error) {
|
||||
@@ -191,7 +191,7 @@ Create a new `Account.svelte` component to handle this functionality.
|
||||
<script lang="ts">
|
||||
import { onMount } from 'svelte'
|
||||
import type { AuthSession } from '@supabase/supabase-js'
|
||||
import { supabaseClient } from '$lib/supabaseClient'
|
||||
import { supabase } from '$lib/supabaseClient'
|
||||
|
||||
export let session: AuthSession
|
||||
|
||||
@@ -209,7 +209,7 @@ Create a new `Account.svelte` component to handle this functionality.
|
||||
loading = true
|
||||
const { user } = session
|
||||
|
||||
const { data, error, status } = await supabaseClient
|
||||
const { data, error, status } = await supabase
|
||||
.from('profiles')
|
||||
.select(`username, website, avatar_url`)
|
||||
.eq('id', user.id)
|
||||
@@ -244,7 +244,7 @@ Create a new `Account.svelte` component to handle this functionality.
|
||||
updated_at: new Date()
|
||||
}
|
||||
|
||||
let { error } = await supabaseClient.from('profiles').upsert(updates)
|
||||
let { error } = await supabase.from('profiles').upsert(updates)
|
||||
|
||||
if (error) throw error
|
||||
} catch (error) {
|
||||
@@ -259,7 +259,7 @@ Create a new `Account.svelte` component to handle this functionality.
|
||||
async function signOut() {
|
||||
try {
|
||||
loading = true
|
||||
let { error } = await supabaseClient.auth.signOut()
|
||||
let { error } = await supabase.auth.signOut()
|
||||
if (error) throw error
|
||||
} catch (error) {
|
||||
if (error instanceof Error) {
|
||||
@@ -344,7 +344,7 @@ Let's create an avatar for the user so that they can upload a profile photo. We
|
||||
```html title="src/routes/Avatar.svelte"
|
||||
<script lang="ts">
|
||||
import { createEventDispatcher } from 'svelte'
|
||||
import { supabaseClient } from '$lib/supabaseClient'
|
||||
import { supabase } from '$lib/supabaseClient'
|
||||
|
||||
export let size = 10
|
||||
export let url: string
|
||||
@@ -357,7 +357,7 @@ Let's create an avatar for the user so that they can upload a profile photo. We
|
||||
|
||||
const downloadImage = async (path: string) => {
|
||||
try {
|
||||
const { data, error } = await supabaseClient.storage.from('avatars').download(path)
|
||||
const { data, error } = await supabase.storage.from('avatars').download(path)
|
||||
|
||||
if (error) {
|
||||
throw error
|
||||
@@ -384,7 +384,7 @@ Let's create an avatar for the user so that they can upload a profile photo. We
|
||||
const fileExt = file.name.split('.').pop()
|
||||
const filePath = `${Math.random()}.${fileExt}`
|
||||
|
||||
let { error } = await supabaseClient.storage.from('avatars').upload(filePath, file)
|
||||
let { error } = await supabase.storage.from('avatars').upload(filePath, file)
|
||||
|
||||
if (error) {
|
||||
throw error
|
||||
|
||||
@@ -246,7 +246,7 @@ There is a new `channel()` interface which are releasing in v2.
|
||||
This is a "preparatory" release for our upcoming [multiplayer](https://supabase.com/blog/supabase-realtime-with-multiplayer-features) features.
|
||||
|
||||
```ts
|
||||
supabaseClient
|
||||
supabase
|
||||
.channel('any_string_you_want')
|
||||
.on('presence', { event: 'track' }, (payload) => {
|
||||
console.log(payload)
|
||||
@@ -258,7 +258,7 @@ As part of this change, the old `from().on().subscribe()` method for listening t
|
||||
|
||||
```ts
|
||||
// v2
|
||||
supabaseClient
|
||||
supabase
|
||||
.channel('any_string_you_want')
|
||||
.on(
|
||||
'postgres_changes',
|
||||
|
||||
+5
@@ -1382,3 +1382,8 @@ You can [find the full code of this tutorial on Github](https://github.com/saimo
|
||||
If you enjoyed the tutorial, you can [find many more tutorials on my YouTube channel](https://www.youtube.com/simongrimmdev_) where I help web developers build awesome mobile apps.
|
||||
|
||||
Until next time and happy coding with Supabase!
|
||||
|
||||
## Resources
|
||||
|
||||
- [Quickstart: Angular](https://supabase.com/docs/guides/with-angular)
|
||||
- [Authentication in Ionic Angular with Supabase](https://supabase.com/blog/authentication-in-ionic-angular)
|
||||
@@ -164,9 +164,9 @@ So how does *this* work? Postgres does the same thing under the covers -- it gen
|
||||
|
||||
Let's say the application using the DB has some [Python](https://python.org) code like the following:
|
||||
|
||||
```sql
|
||||
import randrange from random
|
||||
import User from models;
|
||||
```python
|
||||
from random import randrange
|
||||
from models import User
|
||||
MAX_RANDOM_USER_ID = 1_000_000_000
|
||||
def create_user():
|
||||
"""
|
||||
@@ -175,7 +175,6 @@ def create_user():
|
||||
user_id = randrange(1, MAX_RANDOM_USER_ID)
|
||||
user = User(id=user_id, email="new@example.com", name="new user")
|
||||
db.save(user)
|
||||
|
||||
```
|
||||
|
||||
That *looks* good, but there's a problem -- [random](https://docs.python.org/3/library/random.html) is a *pseudorandom* generator.
|
||||
@@ -348,7 +347,7 @@ Unfortunately, `uuid-ossp` isn't *quite* so advanced as to have many of these ne
|
||||
|
||||
[pg_idkit](https://github.com/t3hmrman/pg_idkit) is built with Rust, so it gives us access to the following ID generation crates:
|
||||
|
||||
- [nanoid](https://crates.io/crates/nanoid) (a [well known package](https://www.npmjs.com/package/nanoid) from the the NodeJS ecosystem)
|
||||
- [nanoid](https://crates.io/crates/nanoid) (a [well known package](https://www.npmjs.com/package/nanoid) from the NodeJS ecosystem)
|
||||
- [ksuid](https://crates.io/crates/ksuid)
|
||||
- [ulid](https://crates.io/crates/ulid)
|
||||
- [rs-snowflake](https://crates.io/crates/rs-snowflake) (Twitter's Snowflake algorithm)
|
||||
|
||||
@@ -79,7 +79,7 @@ It has a lot of potential though. Some ideas we'll be playing with over the next
|
||||
- **Documentation:** for tutorials and demos.
|
||||
- **Offline data:** running it in the browser for an offline cache, similar to [sql.js](https://sql.js.org/#/) or [absurd-sql](https://github.com/jlongster/absurd-sql).
|
||||
- **Offline data analysis:** using it in a dashboard for offline data analysis and charts.
|
||||
- **Testing:** testing PostgresSQL functions, triggers, data modeling, logical replication, etc.
|
||||
- **Testing:** testing PostgreSQL functions, triggers, data modeling, logical replication, etc.
|
||||
- **Dev environments:** use it as a development environment — pull data from production or push new data, functions, triggers, views up to production.
|
||||
- **Snapshots:** create a test version of your database with sample data, then take a snapshot to send to other developers.
|
||||
- **Support:** send snapshots of your database to support personnel to demonstrate an issue you're having.
|
||||
@@ -97,7 +97,7 @@ See [source code](https://github.com/snaplet/postgres-browser/tree/main/packages
|
||||
|
||||
### Web application
|
||||
|
||||
Next, we need to run the the VM inside our browser. How? [WASM](https://en.wikipedia.org/wiki/WebAssembly).
|
||||
Next, we need to run the VM inside our browser. How? [WASM](https://en.wikipedia.org/wiki/WebAssembly).
|
||||
We use [v86](https://github.com/copy/v86) to run our VM inside the browser. Our demo application is very simple - plain HTML and some basic styling.
|
||||
|
||||
See [source code](https://github.com/snaplet/postgres-browser/tree/main/packages/pg-browser).
|
||||
@@ -147,7 +147,7 @@ We achieve this by mounting a compressed [9P filesystem](<https://en.wikipedia.o
|
||||
renames every file an 8-character name and produces a `filesystem.json` file representing a nested structure with files, original file names, sizes, etc.
|
||||
We then [copy this compressed output](https://github.com/supabase-community/postgres-wasm/blob/main/packages/buildroot/config/board/pg-browser/post-image.sh) to the VM.
|
||||
We modified the kernel command line and v86 boot parameters to boot directly from the 9P filesystem, and even put our kernel file into the p9 filesystem.
|
||||
All non-essential files are loaded asyncrhonously over HTTPS in the browser as needed.
|
||||
All non-essential files are loaded asynchronously over HTTPS in the browser as needed.
|
||||
|
||||
The initial state was smaller, but still 15-20mb in size. We discussed this with Fabian who pointed us towards the `page_poison=on` kernel parameter.
|
||||
This parameter allowed us to clear caches before creating the snapshots, by forcing Linux to write arbitrary bytes on freed memory instead of random bytes,
|
||||
|
||||
@@ -348,7 +348,7 @@ _Quentin de Quelen, Meilisearch CEO_
|
||||
|
||||
### Typesense
|
||||
|
||||
> Some more context around Typesense's behavior: how loose or exact matches are done are configurable in Typesense. For eg, you can configure the number of typos that are accounted for (num_typos), and when typo tolerance should kick-in, only if a configurable amount of results are not found (typo_tokens_treshold). For multi keyword searches, you can configure Typesense to expand the search by dropping some of the keywords, until at least a configurable number of search results are found (drop_tokens_threshold).
|
||||
> Some more context around Typesense's behavior: how loose or exact matches are done are configurable in Typesense. For eg, you can configure the number of typos that are accounted for (num_typos), and when typo tolerance should kick-in, only if a configurable amount of results are not found (typo_tokens_threshold). For multi keyword searches, you can configure Typesense to expand the search by dropping some of the keywords, until at least a configurable number of search results are found (drop_tokens_threshold).
|
||||
> We've chosen specifically to keep the search tight because we received feedback from users that some results were confusing if we let the defaults be too loose.
|
||||
|
||||
_Jason Bosco, Typesense CEO_
|
||||
|
||||
@@ -43,7 +43,7 @@ The Auth team published an in-depth doc explaining how Supabase Auth supports se
|
||||
|
||||

|
||||
|
||||
Do you have 100% code coverage? Probably not, because that’s usually the last thing you think of, but *definitely* not if you don’t have database tests. We just shipped a framework for Database Tests which makes it incredibly easy to test your database using pgTAP an pg_prove.
|
||||
Do you have 100% code coverage? Probably not, because that’s usually the last thing you think of, but *definitely* not if you don’t have database tests. We just shipped a framework for Database Tests which makes it incredibly easy to test your database using pgTAP and pg_prove.
|
||||
|
||||
[Read the docs](https://supabase.com/docs/guides/database/testing).
|
||||
|
||||
@@ -96,7 +96,7 @@ If you want to see +100 other apps, check [the replies](https://twitter.com/supa
|
||||
|
||||
## Launch Week 6 date announced
|
||||
|
||||
We are ending 2022 on a high note ✊. Mark your calendars: Launch Week 6 is happenning on **December 12-16**.
|
||||
We are ending 2022 on a high note ✊. Mark your calendars: Launch Week 6 is happening on **December 12-16**.
|
||||
|
||||
In the meantime, [revisit LW5](https://supabase.com/launch-week).
|
||||
|
||||
|
||||
@@ -11,10 +11,6 @@
|
||||
"question": "When will I be billed?",
|
||||
"answer": "Our Pro plan is charged up front, and billed on a monthly basis. Additional usage costs are also billed at the end of the month."
|
||||
},
|
||||
{
|
||||
"question": "Can I cap my additional usage so my bill doesn't run over $25?",
|
||||
"answer": "Please note: The following feature will roll out 11th April 2022. Soft limits are currently in place, and we will contact the project owner if the project usage exceeds the allocated free limits | Pro plan projects have an optional 'Spend cap'. When this is enabled, the project will not go over the included Pro plan allocated limits."
|
||||
},
|
||||
{
|
||||
"question": "Are you going to change your pricing in the future?",
|
||||
"answer": "Our pricing is in Beta. You can read more about our decisions in our [pricing blog post](/blog/2021/03/29/pricing). Pricing may change in the future, however as a team of developers we are committed to pricing being as developer friendly as possible."
|
||||
|
||||
Generated
+4
@@ -80,10 +80,12 @@
|
||||
"config": "*",
|
||||
"ejs": "^3.1.8",
|
||||
"eslint": "8.9.0",
|
||||
"globby": "^12.0.2",
|
||||
"minimist": "^1.2.6",
|
||||
"next-transpile-modules": "9.0.0",
|
||||
"npm-run-all": "^4.1.5",
|
||||
"openapi-types": "^12.0.2",
|
||||
"prettier": "^2.2.1",
|
||||
"sass": "^1.55.0",
|
||||
"ts-node": "^10.9.1",
|
||||
"tsconfig": "*",
|
||||
@@ -48654,6 +48656,7 @@
|
||||
"dotenv": "^16.0.3",
|
||||
"ejs": "^3.1.8",
|
||||
"eslint": "8.9.0",
|
||||
"globby": "^12.0.2",
|
||||
"graphql": "^16.5.0",
|
||||
"gray-matter": "^4.0.3",
|
||||
"jsrsasign": "^10.5.26",
|
||||
@@ -48668,6 +48671,7 @@
|
||||
"npm-run-all": "^4.1.5",
|
||||
"octokit": "^2.0.10",
|
||||
"openapi-types": "^12.0.2",
|
||||
"prettier": "^2.2.1",
|
||||
"react": "17.0.2",
|
||||
"react-copy-to-clipboard": "^5.0.2",
|
||||
"react-dom": "17.0.2",
|
||||
|
||||
@@ -71,6 +71,10 @@ const SupabaseGridLayout = forwardRef<SupabaseGridRef, SupabaseGridProps>((props
|
||||
const sorts = formatSortURLParams(sort as string[])
|
||||
const filters = formatFilterURLParams(filter as string[])
|
||||
|
||||
// [Joshen] This is not a perfect fix, but the useEffect to initialize the data in the editor
|
||||
// was not getting retriggered for views, works fine for tables.
|
||||
const table = typeof props.table === 'string' ? { name: props.table } : props.table
|
||||
|
||||
useImperativeHandle(ref, () => ({
|
||||
rowAdded(row: Dictionary<any>) {
|
||||
dispatch({
|
||||
@@ -91,7 +95,7 @@ const SupabaseGridLayout = forwardRef<SupabaseGridRef, SupabaseGridProps>((props
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
if (state.refreshPageFlag == REFRESH_PAGE_IMMEDIATELY) {
|
||||
if (state.refreshPageFlag === REFRESH_PAGE_IMMEDIATELY) {
|
||||
fetchPage(state, dispatch, sorts, filters)
|
||||
} else if (state.refreshPageFlag !== 0) {
|
||||
refreshPageDebounced(state, dispatch, sorts, filters)
|
||||
@@ -145,15 +149,15 @@ const SupabaseGridLayout = forwardRef<SupabaseGridRef, SupabaseGridProps>((props
|
||||
useEffect(() => {
|
||||
if (!state.metaService) return
|
||||
|
||||
if (
|
||||
!state.table ||
|
||||
(typeof props.table === 'string' &&
|
||||
state.table!.name !== props.table &&
|
||||
state.table!.schema !== props.schema) ||
|
||||
(typeof props.table !== 'string' &&
|
||||
JSON.stringify(props.table) !== JSON.stringify(state.table))
|
||||
) {
|
||||
const { savedState } = initTable(props, state, dispatch, sort as string[], filter as string[])
|
||||
const initializeData = async () => {
|
||||
const { savedState } = await initTable(
|
||||
props,
|
||||
state,
|
||||
dispatch,
|
||||
sort as string[],
|
||||
filter as string[]
|
||||
)
|
||||
|
||||
if (savedState.sorts || savedState.filters) {
|
||||
setParams((prevParams) => {
|
||||
return {
|
||||
@@ -164,7 +168,18 @@ const SupabaseGridLayout = forwardRef<SupabaseGridRef, SupabaseGridProps>((props
|
||||
})
|
||||
}
|
||||
}
|
||||
}, [state.metaService, state.table, props.table, props.schema])
|
||||
|
||||
const refreshView =
|
||||
typeof props.table === 'string' &&
|
||||
state?.table?.name !== props.table &&
|
||||
state?.table?.schema !== props.schema
|
||||
const refreshTable =
|
||||
typeof props.table !== 'string' && JSON.stringify(props.table) !== JSON.stringify(state.table)
|
||||
|
||||
if (!state.table || refreshView || refreshTable) {
|
||||
initializeData()
|
||||
}
|
||||
}, [state.metaService, state.table, table, props.schema])
|
||||
|
||||
return (
|
||||
<div className="sb-grid">
|
||||
|
||||
@@ -67,13 +67,13 @@ export function formatFilterURLParams(filter?: string[]): Filter[] {
|
||||
) as Filter[]
|
||||
}
|
||||
|
||||
export function initTable(
|
||||
export async function initTable(
|
||||
props: SupabaseGridProps,
|
||||
state: InitialStateType,
|
||||
dispatch: (value: any) => void,
|
||||
sort?: string[], // Come directly from URL param
|
||||
filter?: string[] // Come directly from URL param
|
||||
): { savedState: { sorts?: string[]; filters?: string[] } } {
|
||||
): Promise<{ savedState: { sorts?: string[]; filters?: string[] } }> {
|
||||
function onInitTable(table: SupaTable, props: SupabaseGridProps) {
|
||||
const savedState = props.storageRef
|
||||
? onLoadStorage(props.storageRef, table.name, table.schema)
|
||||
@@ -118,19 +118,15 @@ export function initTable(
|
||||
}
|
||||
|
||||
if (typeof props.table === 'string') {
|
||||
const fetchMethod = props.editable
|
||||
? fetchEditableInfo(state.metaService!, props.table, props.schema)
|
||||
: fetchReadOnlyInfo(state.metaService!, props.table, props.schema)
|
||||
const viewData = props.editable
|
||||
? await fetchEditableInfo(state.metaService!, props.table, props.schema)
|
||||
: await fetchReadOnlyInfo(state.metaService!, props.table, props.schema)
|
||||
|
||||
fetchMethod.then((res) => {
|
||||
if (res) {
|
||||
return onInitTable(res, props)
|
||||
} else {
|
||||
if (props.onError) {
|
||||
props.onError({ message: 'fetch table info failed' })
|
||||
}
|
||||
}
|
||||
})
|
||||
if (viewData) {
|
||||
return onInitTable(viewData, props)
|
||||
} else {
|
||||
if (props.onError) props.onError({ message: 'Failed to fetch data from view' })
|
||||
}
|
||||
} else {
|
||||
return onInitTable(props.table, props)
|
||||
}
|
||||
|
||||
@@ -26,7 +26,13 @@ export async function fetchPage(
|
||||
sorts: Sort[],
|
||||
filters: Filter[]
|
||||
) {
|
||||
if (!state.rowService) return
|
||||
if (!state.rowService) {
|
||||
return dispatch({
|
||||
type: 'SET_ROWS',
|
||||
payload: { rows: state?.rows ?? [] },
|
||||
})
|
||||
}
|
||||
|
||||
dispatch({ type: 'SET_IS_LOADING', payload: { isLoading: true } })
|
||||
|
||||
const { data, error } = await state.rowService.fetchPage(
|
||||
|
||||
@@ -50,7 +50,7 @@ const DatabaseUsage: FC<any> = () => {
|
||||
let cancel = false
|
||||
const getDatabaseSize = async () => {
|
||||
const res = await meta.query(
|
||||
'select sum(pg_database_size(pg_database.datname))::integer as db_size from pg_database;'
|
||||
'select sum(pg_database_size(pg_database.datname))::bigint as db_size from pg_database;'
|
||||
)
|
||||
if (!res.error && !cancel) {
|
||||
setDatabaseSize(res[0].db_size)
|
||||
|
||||
Reference in new issue
Block a user