No files matched your search
@@ -19,6 +19,7 @@
|
||||
- [x] Database Functions. [Docs](https://supabase.com/docs/guides/database/functions)
|
||||
- [x] Edge Functions [Docs](https://supabase.com/docs/guides/functions)
|
||||
- [x] File Storage. [Docs](https://supabase.com/docs/guides/storage)
|
||||
- [x] AI + Vector/Embeddings Toolkit. [Docs](https://supabase.com/docs/guides/ai)
|
||||
- [x] Dashboard
|
||||
|
||||

|
||||
@@ -247,8 +248,3 @@ Our approach for client libraries is modular. Each sub-library is a standalone i
|
||||
- [Vietnamese / Tiếng Việt](/i18n/README.vi-vn.md)
|
||||
- [List of translations](/i18n/languages.md) <!--- Keep only this -->
|
||||
|
||||
---
|
||||
|
||||
## Sponsors
|
||||
|
||||
[](https://github.com/sponsors/supabase)
|
||||
@@ -1,13 +1,12 @@
|
||||
import { FC } from 'react'
|
||||
import { IconInfo, IconHelpCircle, IconAlertTriangle } from 'ui'
|
||||
import { PropsWithChildren } from 'react'
|
||||
import { IconAlertTriangle, IconHelpCircle, IconInfo } from 'ui'
|
||||
|
||||
interface Props {
|
||||
export interface AdmonitionProps {
|
||||
type: 'note' | 'tip' | 'info' | 'caution' | 'danger'
|
||||
label?: string
|
||||
children: any
|
||||
}
|
||||
|
||||
const Admonition: FC<Props> = ({ type = 'note', label, children }) => {
|
||||
const Admonition = ({ type = 'note', label, children }: PropsWithChildren<AdmonitionProps>) => {
|
||||
return (
|
||||
<div
|
||||
className={[
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
## Deployment
|
||||
|
||||
If you have your own infrastructure for deploying Python apps, you can continue to use `vecs` as described in this guide.
|
||||
|
||||
Alternatively if you would like to quickly deploy using Supabase, check out our guide on using the [Hugging Face Inference API](/docs/guides/ai/hugging-face) in Edge Functions using TypeScript.
|
||||
@@ -876,6 +876,16 @@ export const ai: NavMenuConstant = {
|
||||
{ name: 'Face similarity search', url: '/guides/ai/quickstarts/face-similarity' },
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'Python Client',
|
||||
url: undefined,
|
||||
items: [
|
||||
{ name: 'API', url: '/guides/ai/python/api' },
|
||||
{ name: 'Collections', url: '/guides/ai/python/collections' },
|
||||
{ name: 'Indexes', url: '/guides/ai/python/indexes' },
|
||||
{ name: 'Metadata', url: '/guides/ai/python/metadata' },
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'Guides',
|
||||
url: undefined,
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
import { Element } from 'hast'
|
||||
import { hasProperty } from 'hast-util-has-property'
|
||||
import { Node } from 'unist'
|
||||
import { visit } from 'unist-util-visit'
|
||||
|
||||
export type UrlTransformFunction = (url: string, node: Element) => string
|
||||
|
||||
function modify(node: Element, prop: string, fn?: UrlTransformFunction) {
|
||||
if (hasProperty(node, prop)) {
|
||||
const property = node.properties[prop]
|
||||
if (typeof property !== 'string') {
|
||||
return
|
||||
}
|
||||
|
||||
node.properties[prop] = fn?.(property, node) ?? property
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Transforms every HAST element that contains a `href` or `src`.
|
||||
* A `UrlTransformFunction` is called with the current URL. The
|
||||
* return value from this function will be used as the replacement.
|
||||
*/
|
||||
export function linkTransform(fn?: UrlTransformFunction) {
|
||||
return function transformer(tree: Node) {
|
||||
visit(tree, 'element', (node: Element) => {
|
||||
modify(node, 'href', fn)
|
||||
modify(node, 'src', fn)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,105 @@
|
||||
import { Content, Paragraph, Parent } from 'mdast'
|
||||
import { MdxJsxFlowElement } from 'mdast-util-mdx'
|
||||
import { Node } from 'unist'
|
||||
import { visit } from 'unist-util-visit'
|
||||
import { AdmonitionProps } from '~/components/Admonition'
|
||||
|
||||
/**
|
||||
* Transforms an `mkdocs-material` Admonition to a Supabase Admonition.
|
||||
*
|
||||
* https://squidfunk.github.io/mkdocs-material/reference/admonitions/
|
||||
*/
|
||||
const remarkMkDocsAdmonition = function () {
|
||||
return function transformer(root: Parent) {
|
||||
visit(root, 'paragraph', (paragraph: Paragraph, index: number, parent: Parent) => {
|
||||
const [firstChild] = paragraph.children
|
||||
|
||||
if (firstChild?.type === 'text') {
|
||||
const match = firstChild.value.match(/^!!! ?(.*?)\n(.*)/s)
|
||||
|
||||
if (!match) {
|
||||
return
|
||||
}
|
||||
|
||||
// Extract the admonition type along with the remaining text
|
||||
const [, type, value] = match
|
||||
|
||||
// Rewrite the node's value to remove the admonition syntax
|
||||
firstChild.value = value
|
||||
|
||||
// Extract sibling nodes that should be linked to this admonition
|
||||
const siblingsToNest = extractLinkedSiblings(parent, paragraph, index)
|
||||
|
||||
const children: any[] = [...paragraph.children, ...siblingsToNest]
|
||||
|
||||
// Generate a Supabase Admonition JSX element
|
||||
const admonitionElement: MdxJsxFlowElement = {
|
||||
type: 'mdxJsxFlowElement',
|
||||
name: 'Admonition',
|
||||
attributes: [
|
||||
{
|
||||
type: 'mdxJsxAttribute',
|
||||
name: 'type',
|
||||
value: mapAdmonitionType(type),
|
||||
},
|
||||
],
|
||||
children,
|
||||
}
|
||||
|
||||
// Overwrite original node with new element
|
||||
parent.children.splice(index, 1, admonitionElement)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Identifies sibling nodes that should be linked to this admonition
|
||||
* based on their indent level (ie. 4 spaces).
|
||||
*
|
||||
* Iterates through proceeding siblings until one is found that is
|
||||
* not indented relative to the original node.
|
||||
*
|
||||
* Splices the discovered siblings out of the original parent and returns them.
|
||||
*/
|
||||
function extractLinkedSiblings(parent: Parent, node: Node, index: number, indentAmount = 4) {
|
||||
const { column } = node.position.start
|
||||
|
||||
let nextSibling: Content
|
||||
let i = index
|
||||
|
||||
do {
|
||||
nextSibling = parent.children[++i]
|
||||
} while (nextSibling?.position && nextSibling.position.start.column === column + indentAmount)
|
||||
|
||||
return parent.children.splice(index + 1, i - index - 1)
|
||||
}
|
||||
|
||||
/**
|
||||
* Maps `mkdocs-material` Admonition types to Supabase Admonition types.
|
||||
*
|
||||
* https://squidfunk.github.io/mkdocs-material/reference/admonitions/#supported-types
|
||||
*/
|
||||
function mapAdmonitionType(type: string): AdmonitionProps['type'] {
|
||||
switch (type) {
|
||||
case 'quote':
|
||||
case 'example':
|
||||
case 'note':
|
||||
return 'note'
|
||||
case 'tip':
|
||||
return 'tip'
|
||||
case 'warning':
|
||||
return 'caution'
|
||||
case 'failure':
|
||||
case 'bug':
|
||||
case 'danger':
|
||||
return 'danger'
|
||||
case 'abstract':
|
||||
case 'question':
|
||||
case 'info':
|
||||
default:
|
||||
return 'info'
|
||||
}
|
||||
}
|
||||
|
||||
export default remarkMkDocsAdmonition
|
||||
@@ -0,0 +1,23 @@
|
||||
import { Parent } from 'mdast'
|
||||
|
||||
/**
|
||||
* Removes the top heading from a MD file if
|
||||
* it is the first node and it matches `title`.
|
||||
*
|
||||
* Useful when rendering title separately from MD
|
||||
* and you need to remove the duplicate.
|
||||
*/
|
||||
export function removeTitle(title: string) {
|
||||
return function transformer(root: Parent) {
|
||||
const [firstNode] = root.children
|
||||
|
||||
if (firstNode?.type === 'heading') {
|
||||
const [text] = firstNode.children
|
||||
|
||||
if (text?.type === 'text' && text.value === title) {
|
||||
// Remove this node
|
||||
root.children.splice(0, 1)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -65,6 +65,7 @@
|
||||
"framer-motion": "^6.5.1",
|
||||
"github-slugger": "^2.0.0",
|
||||
"gray-matter": "^4.0.3",
|
||||
"hast-util-has-property": "^2.0.1",
|
||||
"isbot": "^3.6.5",
|
||||
"jsrsasign": "^10.5.26",
|
||||
"lodash": "^4.17.21",
|
||||
@@ -101,12 +102,17 @@
|
||||
"ui": "*",
|
||||
"unist-builder": "^3.0.1",
|
||||
"unist-util-filter": "^4.0.1",
|
||||
"unist-util-visit": "^4.1.2",
|
||||
"uuid": "^9.0.0",
|
||||
"valtio": "^1.7.6"
|
||||
"valtio": "^1.7.6",
|
||||
"yargs": "^17.7.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/hast": "^2.3.4",
|
||||
"@types/node": "^17.0.12",
|
||||
"@types/react": "17.0.39",
|
||||
"@types/unist": "^2.0.6",
|
||||
"@types/yargs": "^17.0.24",
|
||||
"config": "*",
|
||||
"dotenv": "^16.0.3",
|
||||
"ejs": "^3.1.8",
|
||||
|
||||
@@ -3,21 +3,16 @@ import Layout from '~/layouts/DefaultGuideLayout'
|
||||
export const meta = {
|
||||
id: 'ai',
|
||||
title: 'AI & Vectors',
|
||||
description: 'Use Supabase to store and search embedding vectors.',
|
||||
subtitle: 'Use Supabase to store and search embedding vectors.',
|
||||
description: 'The best vector database is the database you already have.',
|
||||
subtitle: 'The best vector database is the database you already have.',
|
||||
sidebar_label: 'Overview',
|
||||
}
|
||||
|
||||
Supabase provides a number of tools to get started with Vectors and Embeddings. These tools include:
|
||||
Supabase provides an open source toolkit for developing AI applications using Postgres and pgvector. Use the Supabase client libraries to store, index, and query your vector embeddings at scale.
|
||||
|
||||
- An embedding store using the popular [pgvector](https://github.com/pgvector/pgvector/)
|
||||
- A Python Library for managing unstructured collections called [Vecs](https://supabase.github.io/vecs/api)
|
||||
The toolkit includes:
|
||||
|
||||
## Features
|
||||
|
||||
Supabase provides a full toolkit for developing AI applications.
|
||||
|
||||
- A [vector store](/docs/guides/ai/vector-columns) and embeddings support using pgvector.
|
||||
- A [vector store](/docs/guides/ai/vector-columns) and embeddings support using Postgres and pgvector.
|
||||
- A [Python client](/docs/guides/ai/vecs-python-client) for managing unstructured embeddings.
|
||||
- [Database migrations](/docs/guides/ai/examples/headless-vector-search#prepare-your-database) for managing structured embeddings.
|
||||
- Integrations with all popular AI providers, such as [OpenAI](/docs/guides/ai/examples/openai), [Hugging Face](/docs/guides/ai/hugging-face), [LangChain](/docs/guides/ai/langchain), and more.
|
||||
@@ -74,6 +69,41 @@ export const examples = [
|
||||
},
|
||||
]
|
||||
|
||||
## Integrations
|
||||
|
||||
<div className="grid md:grid-cols-12 gap-4 not-prose">
|
||||
{integrations.map((x) => (
|
||||
<div className="col-span-4" key={x.href}>
|
||||
<Link href={x.href} passHref>
|
||||
<a>
|
||||
<GlassPanel title={x.name}>{x.description}</GlassPanel>
|
||||
</a>
|
||||
</Link>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
export const integrations = [
|
||||
{
|
||||
name: 'OpenAI',
|
||||
description:
|
||||
'OpenAI is an AI research and deployment company. Supabase provides a simple way to use OpenAI in your applications.',
|
||||
href: '/docs/guides/ai/examples/building-chatgpt-plugins',
|
||||
},
|
||||
{
|
||||
name: 'Hugging Face',
|
||||
description:
|
||||
"Hugging Face is an open-source provider of NLP technologies. Supabase provides a simple way to use Hugging Face's models in your applications.",
|
||||
href: '/docs/guides/ai/hugging-face',
|
||||
},
|
||||
{
|
||||
name: 'LangChain',
|
||||
description:
|
||||
'LangChain is a language-agnostic, open-source, and self-hosted API for text translation, summarization, and sentiment analysis.',
|
||||
href: '/docs/guides/ai/langchain',
|
||||
},
|
||||
]
|
||||
|
||||
## Case studies
|
||||
|
||||
<div className="grid md:grid-cols-12 gap-4 not-prose">
|
||||
|
||||
@@ -9,6 +9,8 @@ export const meta = {
|
||||
|
||||
[Hugging Face](https://huggingface.co) is an open source hub for AI/ML models and tools. With over 100,000 machine learning models available, Hugging Face provides a great way to integrate specialized AI & ML tasks into your application.
|
||||
|
||||
Hugging Face exposes an [Inference API](https://huggingface.co/inference-api) you can use to execute AI tasks remotely on Hugging Face servers. This opens the doors to using Hugging Face with languages like TypeScript and can be deployed using [Edge Functions](/docs/guides/functions).
|
||||
|
||||
## AI Tasks
|
||||
|
||||
Below are some of the types of tasks you can perform with Hugging Face:
|
||||
@@ -40,7 +42,7 @@ See a [full list of tasks](https://huggingface.co/tasks).
|
||||
|
||||
## Access token
|
||||
|
||||
You should first generate a Hugging Face access token for your app:
|
||||
First generate a Hugging Face access token for your app:
|
||||
|
||||
https://huggingface.co/settings/tokens
|
||||
|
||||
@@ -59,13 +61,9 @@ To ensure you don't experience any unexpected downtime or errors, we recommend c
|
||||
|
||||
</Admonition>
|
||||
|
||||
## Integrate with Supabase
|
||||
## Edge Functions
|
||||
|
||||
Supabase aims to make it as easy as possible to integrate with third-party tools like Hugging Face.
|
||||
|
||||
### Edge functions
|
||||
|
||||
When building any application, you should never trust the client frontend with secrets or credentials. Here this means you should never use your Hugging Face access token directly from your frontend. Supabase solves this using [Edge Functions](/docs/guides/functions), server side TypeScript functions that spin up on-demand only as you need them. Since Edge Functions run on a server, you can safely give them access to your Hugging Face access token.
|
||||
Edge Functions are server-side TypeScript functions that run on-demand. Since Edge Functions run on a server, you can safely give them access to your Hugging Face access token.
|
||||
|
||||
<Admonition type="info">
|
||||
|
||||
@@ -73,13 +71,13 @@ You will need the `supabase` CLI [installed](/docs/guides/cli) for the following
|
||||
|
||||
</Admonition>
|
||||
|
||||
To create a new edge function, navigate to your local project and initialize Supabase if you haven't already:
|
||||
To create a new Edge Function, navigate to your local project and initialize Supabase if you haven't already:
|
||||
|
||||
```shell
|
||||
supabase init
|
||||
```
|
||||
|
||||
Then create an edge function:
|
||||
Then create an Edge Function:
|
||||
|
||||
```shell
|
||||
supabase functions new text-to-image
|
||||
@@ -91,7 +89,7 @@ Create a file called `.env.local` to store your Hugging Face access token:
|
||||
HUGGING_FACE_ACCESS_TOKEN=<your-token-here>
|
||||
```
|
||||
|
||||
Let's modify the edge function to import Hugging Face's inference client and perform a text-to-image request:
|
||||
Let's modify the Edge Function to import Hugging Face's inference client and perform a `text-to-image` request:
|
||||
|
||||
```ts
|
||||
import { serve } from 'https://deno.land/std@0.168.0/http/server.ts'
|
||||
@@ -116,31 +114,31 @@ serve(async (req) => {
|
||||
})
|
||||
```
|
||||
|
||||
This function creates a new instance of `HfInference` using the `HUGGING_FACE_ACCESS_TOKEN` environment variable.
|
||||
1. This function creates a new instance of `HfInference` using the `HUGGING_FACE_ACCESS_TOKEN` environment variable.
|
||||
|
||||
It expects a POST request that includes a JSON request body. The JSON body should include a parameter called `prompt` that represents the text-to-image prompt that we will pass to Hugging Face's inference API.
|
||||
1. It expects a POST request that includes a JSON request body. The JSON body should include a parameter called `prompt` that represents the text-to-image prompt that we will pass to Hugging Face's inference API.
|
||||
|
||||
Next we call `textToImage()`, passing in the user's prompt along with the model that we would like to use for the image generation. Today Hugging Face recommends `stabilityai/stable-diffusion-2`, but you can change this to any other text-to-image model. You can see a list of which models are supported for each task by navigating to their [models page](https://huggingface.co/models?pipeline_tag=text-to-image) and filtering by task.
|
||||
1. Next we call `textToImage()`, passing in the user's prompt along with the model that we would like to use for the image generation. Today Hugging Face recommends `stabilityai/stable-diffusion-2`, but you can change this to any other text-to-image model. You can see a list of which models are supported for each task by navigating to their [models page](https://huggingface.co/models?pipeline_tag=text-to-image) and filtering by task.
|
||||
|
||||
We set `use_cache` to `false` so that repeat queries with the same prompt will produce new images. If the task and model you are using is deterministic (will always produce the same result based on the same input), consider setting `use_cache` to `true` for faster responses.
|
||||
1. We set `use_cache` to `false` so that repeat queries with the same prompt will produce new images. If the task and model you are using is deterministic (will always produce the same result based on the same input), consider setting `use_cache` to `true` for faster responses.
|
||||
|
||||
The `image` result returned from the API will be a `Blob`. We can pass the `Blob` directly into a `new Response()` which will automatically set the content type and body of the response from the `image`.
|
||||
1. The `image` result returned from the API will be a `Blob`. We can pass the `Blob` directly into a `new Response()` which will automatically set the content type and body of the response from the `image`.
|
||||
|
||||
Finally let's serve the edge function locally to test it:
|
||||
Finally let's serve the Edge Function locally to test it:
|
||||
|
||||
```shell
|
||||
supabase functions serve --env-file .env.local --no-verify-jwt
|
||||
```
|
||||
|
||||
Remember to pass in the `.env.local` file using the `--env-file` parameter so that the edge function can access the `HUGGING_FACE_ACCESS_TOKEN`.
|
||||
Remember to pass in the `.env.local` file using the `--env-file` parameter so that the Edge Function can access the `HUGGING_FACE_ACCESS_TOKEN`.
|
||||
|
||||
<Admonition type="info">
|
||||
|
||||
For demo purposes we set `--no-verify-jwt` to make it easy to test the edge function without passing in a JWT token. In a real application you will need to pass the JWT as a `Bearer` token in the `Authorization` header.
|
||||
For demo purposes we set `--no-verify-jwt` to make it easy to test the Edge Function without passing in a JWT token. In a real application you will need to pass the JWT as a `Bearer` token in the `Authorization` header.
|
||||
|
||||
</Admonition>
|
||||
|
||||
At this point, you can make an API request to your edge function using your preferred frontend framework (Next.js, React, Expo, etc). We can also test from the terminal using `curl`:
|
||||
At this point, you can make an API request to your Edge Function using your preferred frontend framework (Next.js, React, Expo, etc). We can also test from the terminal using `curl`:
|
||||
|
||||
```shell
|
||||
curl --output result.jpg --location --request POST 'http://localhost:54321/functions/v1/text-to-image' \
|
||||
@@ -148,7 +146,7 @@ curl --output result.jpg --location --request POST 'http://localhost:54321/funct
|
||||
--data '{"query":"Llama wearing sunglasses"}'
|
||||
```
|
||||
|
||||
Your generated image will save to `result.jpg`:
|
||||
In this example, your generated image will save to `result.jpg`:
|
||||
|
||||
<img
|
||||
src="/docs/img/ai/hugging-face/llama-sunglasses-example.png"
|
||||
@@ -157,6 +155,12 @@ Your generated image will save to `result.jpg`:
|
||||
height="400"
|
||||
/>
|
||||
|
||||
## Next steps
|
||||
|
||||
You can now create an Edge Function that invokes a Hugging Face task using your model of choice.
|
||||
|
||||
Try running some other [AI tasks](#ai-tasks).
|
||||
|
||||
## Resources
|
||||
|
||||
- Official [Hugging Face site](https://huggingface.co/).
|
||||
|
||||
@@ -29,7 +29,7 @@ create table documents (
|
||||
-- Create a function to search for documents
|
||||
create function match_documents (
|
||||
query_embedding vector(1536),
|
||||
match_count int,
|
||||
match_count int default null,
|
||||
filter jsonb DEFAULT '{}'
|
||||
) returns table (
|
||||
id bigint,
|
||||
@@ -90,6 +90,114 @@ export const run = async () => {
|
||||
}
|
||||
```
|
||||
|
||||
### Simple Metadata Filtering
|
||||
|
||||
Given the above `match_documents` Postgres function, you can also pass a filter parameter to only return documents with a specific metadata field value. This filter parameter is a JSON object, and the `match_documents` function will use the Postgres JSONB Containment operator `@>` to filter documents by the metadata field values you specify. See details on the [Postgres JSONB Containment operator](https://www.postgresql.org/docs/current/datatype-json.html#JSON-CONTAINMENT) for more information.
|
||||
|
||||
```js
|
||||
import { SupabaseVectorStore } from 'langchain/vectorstores/supabase'
|
||||
import { OpenAIEmbeddings } from 'langchain/embeddings/openai'
|
||||
import { createClient } from '@supabase/supabase-js'
|
||||
|
||||
// First, follow set-up instructions above
|
||||
|
||||
const privateKey = process.env.SUPABASE_SERVICE_ROLE_KEY
|
||||
if (!privateKey) throw new Error(`Expected env var SUPABASE_SERVICE_ROLE_KEY`)
|
||||
|
||||
const url = process.env.SUPABASE_URL
|
||||
if (!url) throw new Error(`Expected env var SUPABASE_URL`)
|
||||
|
||||
export const run = async () => {
|
||||
const client = createClient(url, privateKey)
|
||||
|
||||
const vectorStore = await SupabaseVectorStore.fromTexts(
|
||||
['Hello world', 'Hello world', 'Hello world'],
|
||||
[{ user_id: 2 }, { user_id: 1 }, { user_id: 3 }],
|
||||
new OpenAIEmbeddings(),
|
||||
{
|
||||
client,
|
||||
tableName: 'documents',
|
||||
queryName: 'match_documents',
|
||||
}
|
||||
)
|
||||
|
||||
const result = await vectorStore.similaritySearch('Hello world', 1, {
|
||||
user_id: 3,
|
||||
})
|
||||
|
||||
console.log(result)
|
||||
}
|
||||
```
|
||||
|
||||
### Advanced Metadata Filtering
|
||||
|
||||
You can also use query builder-style filtering ([similar to how the Supabase JavaScript library works](https://supabase.com/docs/reference/javascript/using-filters)) instead of passing an object. Note that since the filter properties will be in the metadata column, you need to use arrow operators (`->` for integer or `->>` for text) as defined in [Postgrest API documentation](https://postgrest.org/en/stable/references/api/tables_views.html?highlight=operators#json-columns) and specify the data type of the property (e.g. the column should look something like `metadata->some_int_value::int`).
|
||||
|
||||
```js
|
||||
import { SupabaseFilterRPCCall, SupabaseVectorStore } from 'langchain/vectorstores/supabase'
|
||||
import { OpenAIEmbeddings } from 'langchain/embeddings/openai'
|
||||
import { createClient } from '@supabase/supabase-js'
|
||||
|
||||
// First, follow set-up instructions above
|
||||
|
||||
const privateKey = process.env.SUPABASE_SERVICE_ROLE_KEY
|
||||
if (!privateKey) throw new Error(`Expected env var SUPABASE_SERVICE_ROLE_KEY`)
|
||||
|
||||
const url = process.env.SUPABASE_URL
|
||||
if (!url) throw new Error(`Expected env var SUPABASE_URL`)
|
||||
|
||||
export const run = async () => {
|
||||
const client = createClient(url, privateKey)
|
||||
|
||||
const embeddings = new OpenAIEmbeddings()
|
||||
|
||||
const store = new SupabaseVectorStore(embeddings, {
|
||||
client,
|
||||
tableName: 'documents',
|
||||
})
|
||||
|
||||
const docs = [
|
||||
{
|
||||
pageContent:
|
||||
'This is a long text, but it actually means something because vector database does not understand Lorem Ipsum. So I would need to expand upon the notion of quantum fluff, a theorectical concept where subatomic particles coalesce to form transient multidimensional spaces. Yet, this abstraction holds no real-world application or comprehensible meaning, reflecting a cosmic puzzle.',
|
||||
metadata: { b: 1, c: 10, stuff: 'right' },
|
||||
},
|
||||
{
|
||||
pageContent:
|
||||
'This is a long text, but it actually means something because vector database does not understand Lorem Ipsum. So I would need to proceed by discussing the echo of virtual tweets in the binary corridors of the digital universe. Each tweet, like a pixelated canary, hums in an unseen frequency, a fascinatingly perplexing phenomenon that, while conjuring vivid imagery, lacks any concrete implication or real-world relevance, portraying a paradox of multidimensional spaces in the age of cyber folklore.',
|
||||
metadata: { b: 2, c: 9, stuff: 'right' },
|
||||
},
|
||||
{ pageContent: 'hello', metadata: { b: 1, c: 9, stuff: 'right' } },
|
||||
{ pageContent: 'hello', metadata: { b: 1, c: 9, stuff: 'wrong' } },
|
||||
{ pageContent: 'hi', metadata: { b: 2, c: 8, stuff: 'right' } },
|
||||
{ pageContent: 'bye', metadata: { b: 3, c: 7, stuff: 'right' } },
|
||||
{ pageContent: "what's this", metadata: { b: 4, c: 6, stuff: 'right' } },
|
||||
]
|
||||
|
||||
await store.addDocuments(docs)
|
||||
|
||||
const funcFilterA: SupabaseFilterRPCCall = (rpc) =>
|
||||
rpc
|
||||
.filter('metadata->b::int', 'lt', 3)
|
||||
.filter('metadata->c::int', 'gt', 7)
|
||||
.textSearch('content', `'multidimensional' & 'spaces'`, {
|
||||
config: 'english',
|
||||
})
|
||||
|
||||
const resultA = await store.similaritySearch('quantum', 4, funcFilterA)
|
||||
|
||||
const funcFilterB: SupabaseFilterRPCCall = (rpc) =>
|
||||
rpc
|
||||
.filter('metadata->b::int', 'lt', 3)
|
||||
.filter('metadata->c::int', 'gt', 7)
|
||||
.filter('metadata->>stuff', 'eq', 'right')
|
||||
|
||||
const resultB = await store.similaritySearch('hello', 2, funcFilterB)
|
||||
|
||||
console.log(resultA, resultB)
|
||||
}
|
||||
```
|
||||
|
||||
## Hybrid search
|
||||
|
||||
LangChain supports the concept of a hybrid search, which combines Similarity Search with Full Text Search. Read the official docs to get started: [Supabase Hybrid Search](https://js.langchain.com/docs/modules/indexes/retrievers/supabase-hybrid).
|
||||
|
||||
@@ -0,0 +1,138 @@
|
||||
import { GetStaticPaths, GetStaticProps } from 'next'
|
||||
import { MDXRemote, MDXRemoteSerializeResult } from 'next-mdx-remote'
|
||||
import { serialize } from 'next-mdx-remote/serialize'
|
||||
import { join, relative } from 'path'
|
||||
import rehypeSlug from 'rehype-slug'
|
||||
import remarkGfm from 'remark-gfm'
|
||||
import components from '~/components'
|
||||
import Layout from '~/layouts/DefaultGuideLayout'
|
||||
import { UrlTransformFunction, linkTransform } from '~/lib/mdx/plugins/rehypeLinkTransform'
|
||||
import remarkMkDocsAdmonition from '~/lib/mdx/plugins/remarkAdmonition'
|
||||
import { removeTitle } from '~/lib/mdx/plugins/remarkRemoveTitle'
|
||||
|
||||
// We fetch these docs at build time from an external repo
|
||||
const org = 'supabase'
|
||||
const repo = 'vecs'
|
||||
const branch = 'main'
|
||||
const docsDir = 'docs'
|
||||
const externalSite = 'https://supabase.github.io/vecs'
|
||||
|
||||
// Each external docs page is mapped to a local page
|
||||
const pageMap = [
|
||||
{
|
||||
slug: 'api',
|
||||
meta: {
|
||||
title: 'API',
|
||||
},
|
||||
remoteFile: 'api.md',
|
||||
},
|
||||
{
|
||||
slug: 'collections',
|
||||
meta: {
|
||||
title: 'Collections',
|
||||
},
|
||||
remoteFile: 'concepts_collections.md',
|
||||
},
|
||||
{
|
||||
slug: 'indexes',
|
||||
meta: {
|
||||
title: 'Indexes',
|
||||
},
|
||||
remoteFile: 'concepts_indexes.md',
|
||||
},
|
||||
{
|
||||
slug: 'metadata',
|
||||
meta: {
|
||||
title: 'Metadata',
|
||||
},
|
||||
remoteFile: 'concepts_metadata.md',
|
||||
},
|
||||
]
|
||||
|
||||
interface PythonClientDocsProps {
|
||||
source: MDXRemoteSerializeResult
|
||||
meta: {
|
||||
title: string
|
||||
description?: string
|
||||
}
|
||||
}
|
||||
|
||||
export default function PythonClientDocs({ source, meta }: PythonClientDocsProps) {
|
||||
return (
|
||||
<Layout meta={meta}>
|
||||
<MDXRemote {...source} components={components} />
|
||||
</Layout>
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch markdown from external repo and transform links
|
||||
*/
|
||||
export const getStaticProps: GetStaticProps<PythonClientDocsProps> = async ({ params }) => {
|
||||
const page = pageMap.find(({ slug }) => slug === params.slug)
|
||||
|
||||
if (!page) {
|
||||
throw new Error(`No page mapping found for slug '${params.slug}'`)
|
||||
}
|
||||
|
||||
const { remoteFile, meta } = page
|
||||
|
||||
const response = await fetch(
|
||||
`https://raw.githubusercontent.com/${org}/${repo}/${branch}/${docsDir}/${remoteFile}`
|
||||
)
|
||||
|
||||
const source = await response.text()
|
||||
|
||||
const urlTransform: UrlTransformFunction = (url) => {
|
||||
try {
|
||||
const externalSiteUrl = new URL(externalSite)
|
||||
|
||||
const placeholderHostname = 'placeholder'
|
||||
const { hostname, pathname, hash } = new URL(url, `http://${placeholderHostname}`)
|
||||
|
||||
// Don't modify a url with a FQDN or a url that's only a hash
|
||||
if (hostname !== placeholderHostname || pathname === '/') {
|
||||
return url
|
||||
}
|
||||
|
||||
const relativePage = (
|
||||
pathname.endsWith('.md')
|
||||
? pathname.replace(/\.md$/, '')
|
||||
: relative(externalSiteUrl.pathname, pathname)
|
||||
).replace(/^\//, '')
|
||||
|
||||
const page = pageMap.find(({ remoteFile }) => `${relativePage}.md` === remoteFile)
|
||||
|
||||
// If we have a mapping for this page, use the mapped path
|
||||
if (page) {
|
||||
return page.slug + hash
|
||||
}
|
||||
|
||||
// If we don't have this page in our docs, link to original docs
|
||||
return `${externalSite}/${relativePage}${hash}`
|
||||
} catch (err) {
|
||||
console.error('Error transforming markdown URL', err)
|
||||
return url
|
||||
}
|
||||
}
|
||||
|
||||
const mdxSource = await serialize(source, {
|
||||
mdxOptions: {
|
||||
remarkPlugins: [remarkGfm, remarkMkDocsAdmonition, [removeTitle, meta.title]],
|
||||
rehypePlugins: [[linkTransform, urlTransform], rehypeSlug],
|
||||
},
|
||||
})
|
||||
|
||||
return { props: { source: mdxSource, meta } }
|
||||
}
|
||||
|
||||
export const getStaticPaths: GetStaticPaths = async () => {
|
||||
return {
|
||||
paths: pageMap.map(({ slug }) => ({
|
||||
params: {
|
||||
slug,
|
||||
},
|
||||
})),
|
||||
fallback: false,
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,4 @@
|
||||
import Layout from '~/layouts/DefaultGuideLayout'
|
||||
import StepHikeCompact from '~/components/StepHikeCompact'
|
||||
|
||||
export const meta = {
|
||||
id: 'ai-vecs-python-client',
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import Layout from '~/layouts/DefaultGuideLayout'
|
||||
import StepHikeCompact from '~/components/StepHikeCompact'
|
||||
|
||||
export const meta = {
|
||||
id: 'ai-vecs-python-client',
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import Layout from '~/layouts/DefaultGuideLayout'
|
||||
import StepHikeCompact from '~/components/StepHikeCompact'
|
||||
import HuggingFaceDeployment from '~/components/MDX/ai/quickstart_hf_deployment.mdx'
|
||||
|
||||
export const meta = {
|
||||
id: 'ai-vecs-python-client',
|
||||
@@ -54,6 +54,8 @@ You can view the inserted items in the [Table Editor](https://app.supabase.com/p
|
||||
|
||||

|
||||
|
||||
<HuggingFaceDeployment />
|
||||
|
||||
## Next steps
|
||||
|
||||
You can now start building your own applications with Vecs. Check our [examples](/docs/guides/ai#examples) for ideas.
|
||||
|
||||
@@ -13,9 +13,16 @@ The Supabase CLI provides the tools you need to manage multiple environments.
|
||||
This guide shows you how to set up your local Supabase development environment that integrates with GitHub Actions to automatically
|
||||
test and release schema changes to staging and production Supabase projects.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
Make sure you have these installed on your local machine:
|
||||
|
||||
- [Docker Desktop](https://docs.docker.com/desktop/)
|
||||
- [Supabase CLI](/docs/guides/cli)
|
||||
- [Git](https://github.com/git-guides/install-git)
|
||||
|
||||
To get started:
|
||||
|
||||
- [Install the Supabase CLI](/docs/guides/cli)
|
||||
- Create a [Supabase project](https://app.supabase.com) or use an existing one
|
||||
- Initialize a local Git repository
|
||||
|
||||
|
||||
@@ -3,8 +3,8 @@ import StepHikeCompact from '~/components/StepHikeCompact'
|
||||
|
||||
export const meta = {
|
||||
id: 'connecting-to-postgres',
|
||||
title: 'Database Connections',
|
||||
description: 'There are various ways to connect to your Postgres database.',
|
||||
title: 'Connecting to your database',
|
||||
description: 'Explore the options for connecting to your Postgres database.',
|
||||
}
|
||||
|
||||
Supabase provides several options for programmatically connecting to your Postgres database:
|
||||
@@ -98,15 +98,82 @@ This is the most granular option. Connections are returned to the pool after eve
|
||||
|
||||
## Integrations
|
||||
|
||||
### Connecting with psql
|
||||
### Connecting with Drizzle
|
||||
|
||||
[`psql`](https://www.postgresql.org/docs/current/app-psql.html) is a command-line tool that comes with Postgres.
|
||||
[Drizzle ORM](https://github.com/drizzle-team/drizzle-orm) is a TypeScript ORM for SQL databases designed with maximum type safety in mind. You can use their ORM to connect to your database.
|
||||
|
||||
Assuming you've downloaded your SSL certificate to `$HOME/Downloads/prod-supabase.cer`, and your host address is `db.ref.supabase.co` you connect to your database via SSL:
|
||||
<StepHikeCompact>
|
||||
|
||||
```shell
|
||||
psql "sslmode=verify-full sslrootcert=$HOME/Downloads/prod-supabase.cer host=db.ref.supabase.co dbname=postgres user=postgres"
|
||||
```
|
||||
<StepHikeCompact.Step step={1}>
|
||||
|
||||
<StepHikeCompact.Details title="Install">
|
||||
|
||||
Install Drizzle and releated dependencies.
|
||||
|
||||
</StepHikeCompact.Details>
|
||||
|
||||
<StepHikeCompact.Code>
|
||||
|
||||
```shell
|
||||
npm i drizzle-orm postgres
|
||||
npm i -D drizzle-kit
|
||||
```
|
||||
|
||||
</StepHikeCompact.Code>
|
||||
|
||||
</StepHikeCompact.Step>
|
||||
|
||||
<StepHikeCompact.Step step={2}>
|
||||
|
||||
<StepHikeCompact.Details title="Create your models">
|
||||
|
||||
Create a `schema.ts` file and define your models.
|
||||
|
||||
</StepHikeCompact.Details>
|
||||
|
||||
<StepHikeCompact.Code>
|
||||
|
||||
```ts
|
||||
import { pgTable, serial, text, varchar } from "drizzle-orm/pg-core";
|
||||
|
||||
export const users = pgTable('users', {
|
||||
id: serial('id').primaryKey(),
|
||||
fullName: text('full_name'),
|
||||
phone: varchar('phone', { length: 256 }),
|
||||
});
|
||||
```
|
||||
|
||||
</StepHikeCompact.Code>
|
||||
|
||||
</StepHikeCompact.Step>
|
||||
|
||||
<StepHikeCompact.Step step={3}>
|
||||
|
||||
<StepHikeCompact.Details title="Connect">
|
||||
|
||||
Connect to your database using the Connection Pooler for serverless environments, and the Direct Connection for long-running servers.
|
||||
|
||||
</StepHikeCompact.Details>
|
||||
|
||||
<StepHikeCompact.Code>
|
||||
|
||||
```ts
|
||||
import { drizzle } from 'drizzle-orm/postgres-js'
|
||||
import postgres from 'postgres'
|
||||
import { users } from './schema'
|
||||
|
||||
const connectionString = process.env.DATABASE_URL
|
||||
const client = postgres(connectionString)
|
||||
const db = drizzle(client);
|
||||
|
||||
const allUsers = await db.select().from(users);
|
||||
```
|
||||
|
||||
</StepHikeCompact.Code>
|
||||
|
||||
</StepHikeCompact.Step>
|
||||
|
||||
</StepHikeCompact>
|
||||
|
||||
### Connecting with pgAdmin
|
||||
|
||||
@@ -180,6 +247,95 @@ psql "sslmode=verify-full sslrootcert=$HOME/Downloads/prod-supabase.cer host=db.
|
||||
|
||||
</StepHikeCompact>
|
||||
|
||||
### Connecting with psql
|
||||
|
||||
[`psql`](https://www.postgresql.org/docs/current/app-psql.html) is a command-line tool that comes with Postgres.
|
||||
|
||||
Assuming you've downloaded your SSL certificate to `$HOME/Downloads/prod-supabase.cer`, and your host address is `db.ref.supabase.co` you connect to your database via SSL:
|
||||
|
||||
```shell
|
||||
psql "sslmode=verify-full sslrootcert=$HOME/Downloads/prod-supabase.cer host=db.ref.supabase.co dbname=postgres user=postgres"
|
||||
```
|
||||
|
||||
### Connecting with Postgres.js
|
||||
|
||||
[Postgres.js](https://github.com/porsager/postgres) is a full-featured PostgreSQL client for Node.js and Deno.
|
||||
|
||||
<StepHikeCompact>
|
||||
|
||||
<StepHikeCompact.Step step={1}>
|
||||
|
||||
<StepHikeCompact.Details title="Install">
|
||||
|
||||
Install Drizzle and releated dependencies.
|
||||
|
||||
</StepHikeCompact.Details>
|
||||
|
||||
<StepHikeCompact.Code>
|
||||
|
||||
```shell
|
||||
npm i postgres
|
||||
npm i -D drizzle-kit
|
||||
```
|
||||
|
||||
</StepHikeCompact.Code>
|
||||
|
||||
</StepHikeCompact.Step>
|
||||
|
||||
<StepHikeCompact.Step step={2}>
|
||||
|
||||
<StepHikeCompact.Details title="Connect">
|
||||
|
||||
Create a `db.js` file with the connection details. Use the Connection Pooler for serverless environments, and the Direct Connection for long-running servers.
|
||||
|
||||
</StepHikeCompact.Details>
|
||||
|
||||
<StepHikeCompact.Code>
|
||||
|
||||
```ts
|
||||
// db.js
|
||||
import postgres from 'postgres'
|
||||
|
||||
const connectionString = process.env.DATABASE_URL
|
||||
const sql = postgres(connectionString)
|
||||
|
||||
export default sql
|
||||
```
|
||||
|
||||
</StepHikeCompact.Code>
|
||||
|
||||
</StepHikeCompact.Step>
|
||||
|
||||
<StepHikeCompact.Step step={3}>
|
||||
|
||||
<StepHikeCompact.Details title="Execute commands">
|
||||
|
||||
Use the connection to execute commands.
|
||||
|
||||
</StepHikeCompact.Details>
|
||||
|
||||
<StepHikeCompact.Code>
|
||||
|
||||
```ts
|
||||
import sql from './db.js'
|
||||
|
||||
async function getUsersOver(age) {
|
||||
const users = await sql`
|
||||
select name, age
|
||||
from users
|
||||
where age > ${ age }
|
||||
`
|
||||
// users = Result [{ name: "Walter", age: 80 }, { name: 'Murray', age: 68 }, ...]
|
||||
return users
|
||||
}
|
||||
```
|
||||
|
||||
</StepHikeCompact.Code>
|
||||
|
||||
</StepHikeCompact.Step>
|
||||
|
||||
</StepHikeCompact>
|
||||
|
||||
export const Page = ({ children }) => <Layout meta={meta} children={children} />
|
||||
|
||||
export default Page
|
||||
@@ -345,7 +345,7 @@ create schema private;
|
||||
Now we can create tables inside the `private` schema:
|
||||
|
||||
```sql
|
||||
create table salaries (
|
||||
create table private.salaries (
|
||||
id bigint generated by default as identity primary key,
|
||||
salary bigint not null,
|
||||
actor_id bigint not null references public.actors
|
||||
|
||||
@@ -165,7 +165,7 @@ export const examples = [
|
||||
},
|
||||
{
|
||||
name: 'Rate Limiting',
|
||||
description: `Rate Limiting Egde Functions with Upstash Redis.`,
|
||||
description: `Rate Limiting Edge Functions with Upstash Redis.`,
|
||||
href: '/guides/functions/examples/rate-limiting',
|
||||
},
|
||||
]
|
||||
|
||||
@@ -30,7 +30,7 @@ as TypeScript continued to evolve, would be breaking changes for existing code.
|
||||
## Mixing JavaScript and TypeScript
|
||||
|
||||
By default, Deno does not type check JavaScript. This can be changed, and is
|
||||
discussed further in [Configuring TypeScript in Deno](./configuration.md). Deno
|
||||
discussed further in [Configuring TypeScript in Deno](https://deno.com/manual/advanced/typescript/configuration). Deno
|
||||
does support JavaScript importing TypeScript and TypeScript importing
|
||||
JavaScript, in complex scenarios.
|
||||
|
||||
@@ -39,7 +39,7 @@ will "read" all the JavaScript in order to be able to evaluate how it might have
|
||||
an impact on the TypeScript types. The type checker will do the best it can to
|
||||
figure out what the types are of the JavaScript you import into TypeScript,
|
||||
including reading any JSDoc comments. Details of this are discussed in detail in
|
||||
the [Types and type declarations](./types.md) section.
|
||||
the [Types and type declarations](https://deno.com/manual/advanced/typescript/types) section.
|
||||
|
||||
## Type resolution
|
||||
|
||||
@@ -47,7 +47,7 @@ One of the core design principles of Deno is to avoid non-standard module
|
||||
resolution, and this applies to type resolution as well. If you want to utilize
|
||||
JavaScript that has type definitions (e.g. a `.d.ts` file), you have to
|
||||
explicitly tell Deno about this. The details of how this is accomplished are
|
||||
covered in the [Types and type declarations](https://deno.land/manual/advanced/typescript/types) section.
|
||||
covered in the [Types and type declarations](https://deno.com/manual/advanced/typescript/types) section.
|
||||
|
||||
export const Page = ({ children }) => <Layout meta={meta} children={children} />
|
||||
|
||||
|
||||
@@ -38,9 +38,9 @@ The Dashboard provides a wide range of features for setting up your project: cre
|
||||
|
||||
Make sure you have these installed on your local machine:
|
||||
|
||||
- [Docker](https://docs.docker.com/engine/install/)
|
||||
- [Git](https://github.com/git-guides/install-git)
|
||||
- [Docker Desktop](https://docs.docker.com/desktop/)
|
||||
- [Supabase CLI](/docs/guides/cli)
|
||||
- [Git](https://github.com/git-guides/install-git)
|
||||
|
||||
## Log in to the Supabase CLI
|
||||
|
||||
@@ -50,7 +50,7 @@ supabase login
|
||||
|
||||
<Admonition type="note">
|
||||
|
||||
If you installed the Supabase CLI via NPM you may have to use `npx supabase login`.
|
||||
If you installed the Supabase CLI via NPM, you will need to run `npx supabase login` instead.
|
||||
|
||||
</Admonition>
|
||||
|
||||
|
||||
@@ -22,8 +22,7 @@ Let's start building the Svelte app from scratch.
|
||||
|
||||
### Initialize a Svelte app
|
||||
|
||||
We can use the [SvelteKit Skeleton Project](https://kit.svelte.dev/docs) to initialize
|
||||
an app called `supabase-sveltekit` (for this tutorial we will be using TypeScript):
|
||||
We can use the [SvelteKit Skeleton Project](https://kit.svelte.dev/docs) to initialize an app called `supabase-sveltekit` (for this tutorial we will be using TypeScript):
|
||||
|
||||
```bash
|
||||
npm create svelte@latest supabase-sveltekit
|
||||
@@ -118,9 +117,7 @@ Create a new `src/routes/+layout.server.ts` file to handle the session on the se
|
||||
|
||||
```ts title=src/routes/+layout.server.ts
|
||||
// src/routes/+layout.server.ts
|
||||
import type { LayoutServerLoad } from './$types'
|
||||
|
||||
export const load: LayoutServerLoad = async ({ locals: { getSession } }) => {
|
||||
export const load = async ({ locals: { getSession } }) => {
|
||||
return {
|
||||
session: await getSession(),
|
||||
}
|
||||
@@ -136,9 +133,8 @@ Create a new `src/routes/+layout.ts` file to handle the session and the supabase
|
||||
import { invalidate } from '$app/navigation'
|
||||
import { PUBLIC_SUPABASE_ANON_KEY, PUBLIC_SUPABASE_URL } from '$env/static/public'
|
||||
import { createSupabaseLoadClient } from '@supabase/auth-helpers-sveltekit'
|
||||
import type { LayoutLoad } from './$types'
|
||||
|
||||
export const load: LayoutLoad = async ({ fetch, data, depends }) => {
|
||||
export const load = async ({ fetch, data, depends }) => {
|
||||
depends('supabase:auth')
|
||||
|
||||
const supabase = createSupabaseLoadClient({
|
||||
@@ -161,23 +157,24 @@ Update your `src/routes/+layout.svelte`:
|
||||
```svelte title=src/routes/+layout.svelte
|
||||
<!-- src/routes/+layout.svelte -->
|
||||
<script lang="ts">
|
||||
import '../styles.css';
|
||||
import { invalidate } from '$app/navigation';
|
||||
import { onMount } from 'svelte';
|
||||
import '../styles.css'
|
||||
import { invalidate } from '$app/navigation'
|
||||
import { onMount } from 'svelte'
|
||||
|
||||
export let data;
|
||||
export let data
|
||||
|
||||
$: ({ supabase, session } = data);
|
||||
let { supabase, session } = data
|
||||
$: ({ supabase, session } = data)
|
||||
|
||||
onMount(() => {
|
||||
const { data } = supabase.auth.onAuthStateChange((event, _session) => {
|
||||
if (_session?.expires_at !== session?.expires_at) {
|
||||
invalidate('supabase:auth');
|
||||
invalidate('supabase:auth')
|
||||
}
|
||||
});
|
||||
})
|
||||
|
||||
return () => data.subscription.unsubscribe();
|
||||
});
|
||||
return () => data.subscription.unsubscribe()
|
||||
})
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
@@ -206,10 +203,10 @@ Add the `Auth` component to your home page
|
||||
```svelte title=src/routes/+page.svelte
|
||||
<!-- src/routes/+page.svelte -->
|
||||
<script lang="ts">
|
||||
import { Auth } from '@supabase/auth-ui-svelte';
|
||||
import { ThemeSupa } from '@supabase/auth-ui-shared';
|
||||
import { Auth } from '@supabase/auth-ui-svelte'
|
||||
import { ThemeSupa } from '@supabase/auth-ui-shared'
|
||||
|
||||
export let data;
|
||||
export let data
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
@@ -221,7 +218,7 @@ Add the `Auth` component to your home page
|
||||
<Auth
|
||||
supabaseClient={data.supabase}
|
||||
view="magic_link"
|
||||
redirectTo={`${data.url}/logging-in?redirect=/`}
|
||||
redirectTo={`${data.url}/auth/callback`}
|
||||
showLinks={false}
|
||||
appearance={{ theme: ThemeSupa, style: { input: 'color: #fff' } }}
|
||||
/>
|
||||
@@ -231,11 +228,6 @@ Add the `Auth` component to your home page
|
||||
|
||||
Create a `src/routes/+page.server.ts` file that will return our website url to be used in our `redirectTo` above.
|
||||
|
||||
> This is necessary because the current Supabase auth flow uses implicit grant which returns the tokens as part of the url fragment (#).
|
||||
|
||||
> The redirect to a non server protected page will make sure that the client captures the url fragment as we cannot read these on the server
|
||||
> and send it over to the server. This all happens behind the scenes and isn't something you will need to do manually.
|
||||
|
||||
```ts
|
||||
// src/routes/+page.server.ts
|
||||
import { redirect } from '@sveltejs/kit'
|
||||
@@ -253,37 +245,60 @@ export const load: PageServerLoad = async ({ url, locals: { getSession } }) => {
|
||||
}
|
||||
```
|
||||
|
||||
### Loading page
|
||||
### Proof Key for Code Exchange (PKCE)
|
||||
|
||||
Create a `src/routes/logging-in/+page.svelte` file with the code below.
|
||||
As we are employing Proof Key for Code Exchange (PKCE) in our authentication flow, it is necessary to create a server endpoint responsible for exchanging the code for a session.
|
||||
|
||||
This page is used for waiting while your client-side code inside of `src/routes/+layout.ts` sends the tokens over to your server-side code to store it in a cookie.
|
||||
In the following code snippet, we perform the following steps:
|
||||
|
||||
```svelte
|
||||
<!-- src/routes/logging-in/+page.svelte -->
|
||||
<script lang="ts">
|
||||
import { browser } from '$app/environment';
|
||||
import { goto } from '$app/navigation';
|
||||
import { page } from '$app/stores';
|
||||
- Retrieve the code sent back from the Supabase Auth server using the `code` query parameter.
|
||||
- Exchange this code for a session, which we store in our chosen storage mechanism (in this case, cookies).
|
||||
- Finally, we redirect the user to the `account` page.
|
||||
|
||||
export let data;
|
||||
$: {
|
||||
const redirectTo = $page.url.searchParams.get('redirect');
|
||||
<Tabs
|
||||
scrollable
|
||||
size="small"
|
||||
type="underlined"
|
||||
defaultActiveId="js"
|
||||
>
|
||||
<TabPanel id="js" label="JavaScript">
|
||||
|
||||
// check if user has been set in session store then redirect
|
||||
if (browser && data.session) {
|
||||
goto(redirectTo ?? '/account');
|
||||
}
|
||||
}
|
||||
</script>
|
||||
```js title=src/routes/auth/callback/+server.js
|
||||
// src/routes/auth/callback/+server.js
|
||||
import { redirect } from '@sveltejs/kit'
|
||||
|
||||
<section>
|
||||
"Because as we know, there are known knowns; there are things we know we know. We also know there
|
||||
are known unknowns; that is to say we know there are some things we do not know. But there are
|
||||
also unknown unknowns—the ones we don't know we don't know" - Donald Rumsfeld
|
||||
</section>
|
||||
export const GET = async ({ url, locals: { supabase } }) => {
|
||||
const code = url.searchParams.get('code')
|
||||
|
||||
if (code) {
|
||||
await supabase.auth.exchangeCodeForSession(code)
|
||||
}
|
||||
|
||||
throw redirect(303, '/account')
|
||||
}
|
||||
```
|
||||
|
||||
</TabPanel>
|
||||
<TabPanel id="ts" label="TypeScript">
|
||||
|
||||
```ts title=src/routes/auth/callback/+server.ts
|
||||
// src/routes/auth/callback/+server.ts
|
||||
import { redirect } from '@sveltejs/kit'
|
||||
|
||||
export const GET = async ({ url, locals: { supabase } }) => {
|
||||
const code = url.searchParams.get('code')
|
||||
|
||||
if (code) {
|
||||
await supabase.auth.exchangeCodeForSession(code)
|
||||
}
|
||||
|
||||
throw redirect(303, '/account')
|
||||
}
|
||||
```
|
||||
|
||||
</TabPanel>
|
||||
</Tabs>
|
||||
|
||||
### Account page
|
||||
|
||||
After a user is signed in, they need to be able to edit their profile details and manage their account.
|
||||
@@ -292,32 +307,33 @@ Create a new `src/routes/account/+page.svelte` file with the content below.
|
||||
```svelte title=src/routes/account/+page.svelte
|
||||
<!-- src/routes/account/+page.svelte -->
|
||||
<script lang="ts">
|
||||
import { enhance, type SubmitFunction } from '$app/forms';
|
||||
import { enhance, type SubmitFunction } from '$app/forms'
|
||||
|
||||
export let data;
|
||||
export let form;
|
||||
export let data
|
||||
export let form
|
||||
|
||||
let { session, supabase, profile } = data;
|
||||
let { session, supabase, profile } = data
|
||||
$: ({ session, supabase, profile } = data)
|
||||
|
||||
let profileForm: any;
|
||||
let loading = false;
|
||||
let fullName: string = profile?.full_name ?? '';
|
||||
let username: string = profile?.username ?? '';
|
||||
let website: string = profile?.website ?? '';
|
||||
let avatarUrl: string = profile?.avatar_url ?? '';
|
||||
let profileForm: HTMLFormElement
|
||||
let loading = false
|
||||
let fullName: string = profile?.full_name ?? ''
|
||||
let username: string = profile?.username ?? ''
|
||||
let website: string = profile?.website ?? ''
|
||||
let avatarUrl: string = profile?.avatar_url ?? ''
|
||||
|
||||
const handleSubmit: SubmitFunction = () => {
|
||||
loading = true;
|
||||
loading = true
|
||||
return async () => {
|
||||
loading = false;
|
||||
loading = false
|
||||
}
|
||||
}
|
||||
|
||||
const handleSignOut: SubmitFunction = () => {
|
||||
loading = true;
|
||||
loading = true
|
||||
return async ({ update }) => {
|
||||
loading = false;
|
||||
update();
|
||||
loading = false
|
||||
update()
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -373,9 +389,8 @@ and handle all our form actions through the `actions` object.
|
||||
|
||||
```ts
|
||||
import { fail, redirect } from '@sveltejs/kit'
|
||||
import type { Actions, PageServerLoad } from './$types'
|
||||
|
||||
export const load = (async ({ locals: { supabase, getSession } }) => {
|
||||
export const load = async ({ locals: { supabase, getSession } }) => {
|
||||
const session = await getSession()
|
||||
|
||||
if (!session) {
|
||||
@@ -389,7 +404,7 @@ export const load = (async ({ locals: { supabase, getSession } }) => {
|
||||
.single()
|
||||
|
||||
return { session, profile }
|
||||
}) satisfies PageServerLoad
|
||||
}
|
||||
|
||||
export const actions = {
|
||||
update: async ({ request, locals: { supabase, getSession } }) => {
|
||||
@@ -433,7 +448,7 @@ export const actions = {
|
||||
throw redirect(303, '/')
|
||||
}
|
||||
},
|
||||
} satisfies Actions
|
||||
}
|
||||
```
|
||||
|
||||
### Launch!
|
||||
@@ -459,65 +474,68 @@ Let's create an avatar for the user so that they can upload a profile photo. We
|
||||
```svelte title=src/routes/account/Avatar.svelte
|
||||
<!-- src/routes/account/Avatar.svelte -->
|
||||
<script lang="ts">
|
||||
import type { SupabaseClient } from '@supabase/supabase-js';
|
||||
import { createEventDispatcher } from 'svelte';
|
||||
import type { SupabaseClient } from '@supabase/supabase-js'
|
||||
import { createEventDispatcher } from 'svelte'
|
||||
|
||||
export let size = 10;
|
||||
export let url: string;
|
||||
export let supabase: SupabaseClient;
|
||||
export let size = 10
|
||||
export let url: string
|
||||
export let supabase: SupabaseClient
|
||||
|
||||
let avatarUrl: string | null = null;
|
||||
let uploading = false;
|
||||
let files: FileList;
|
||||
let avatarUrl: string | null = null
|
||||
let uploading = false
|
||||
let files: FileList
|
||||
|
||||
const dispatch = createEventDispatcher();
|
||||
const dispatch = createEventDispatcher()
|
||||
|
||||
const downloadImage = async (path: string) => {
|
||||
try {
|
||||
const { data, error } = await supabase.storage.from('avatars').download(path);
|
||||
const { data, error } = await supabase.storage.from('avatars').download(path)
|
||||
|
||||
if (error) {
|
||||
throw error;
|
||||
throw error
|
||||
}
|
||||
|
||||
const url = URL.createObjectURL(data);
|
||||
avatarUrl = url;
|
||||
const url = URL.createObjectURL(data)
|
||||
avatarUrl = url
|
||||
} catch (error) {
|
||||
if (error instanceof Error) {
|
||||
console.log('Error downloading image: ', error.message);
|
||||
console.log('Error downloading image: ', error.message)
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
const uploadAvatar = async () => {
|
||||
try {
|
||||
uploading = true;
|
||||
uploading = true
|
||||
|
||||
if (!files || files.length === 0) {
|
||||
throw new Error('You must select an image to upload.');
|
||||
throw new Error('You must select an image to upload.')
|
||||
}
|
||||
|
||||
const file = files[0];
|
||||
const fileExt = file.name.split('.').pop();
|
||||
url = `${Math.random()}.${fileExt}`;
|
||||
const file = files[0]
|
||||
const fileExt = file.name.split('.').pop()
|
||||
const filePath = `${Math.random()}.${fileExt}`
|
||||
|
||||
let { error } = await supabase.storage.from('avatars').upload(url, file);
|
||||
let { error } = await supabase.storage.from('avatars').upload(filePath, file)
|
||||
|
||||
if (error) {
|
||||
throw error;
|
||||
throw error
|
||||
}
|
||||
|
||||
dispatch('upload');
|
||||
url = filePath
|
||||
setTimeout(() => {
|
||||
dispatch('upload')
|
||||
}, 100)
|
||||
} catch (error) {
|
||||
if (error instanceof Error) {
|
||||
alert(error.message);
|
||||
alert(error.message)
|
||||
}
|
||||
} finally {
|
||||
uploading = false;
|
||||
uploading = false
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
$: if (url) downloadImage(url);
|
||||
$: if (url) downloadImage(url)
|
||||
</script>
|
||||
|
||||
<div>
|
||||
|
||||
@@ -7,17 +7,17 @@ export const meta = {
|
||||
'This guide explains how to connect your Supabase database with Clerk, a powerful authentication provider built for the modern web.',
|
||||
}
|
||||
|
||||
This guide explains how to connect your Supabase database with [Clerk](https://clerk.dev), an authentication provider built for the modern web.
|
||||
This guide explains how to connect your Supabase database with [Clerk](https://clerk.com), an authentication provider built for the modern web.
|
||||
|
||||
Clerk authenticates users, manages session tokens, and provides user management functionality that can be used in combination with the authorization logic available in Supabase through PostgreSQL Row Level Security (RLS) policies.
|
||||
|
||||
This guide assumes you have a Supabase account and database project already set up.
|
||||
|
||||
If you don't have a Clerk account, you can [create one now](https://dashboard.clerk.dev/sign-up).
|
||||
If you don't have a Clerk account, you can [create one now](https://dashboard.clerk.com/sign-up).
|
||||
|
||||
## Step 1: Create JWT template
|
||||
|
||||
The first step is to create a new Clerk application from your Clerk Dashboard if you haven't done so already. You can choose whichever authentication strategy and social login providers you prefer. For more information, check out Clerk's [guide](https://docs.clerk.dev/popular-guides/setup-your-application).
|
||||
The first step is to create a new Clerk application from your Clerk Dashboard if you haven't done so already. You can choose whichever authentication strategy and social login providers you prefer. For more information, check out Clerk's [guide](https://clerk.com/docs/authentication/set-up-your-application).
|
||||
|
||||
After your Clerk application has been created, use the lefthand menu to navigate to the **JWT Templates** page.
|
||||
|
||||
@@ -25,7 +25,7 @@ Click on the button to create a new template based on Supabase.
|
||||
|
||||

|
||||
|
||||
This will pre-populate the default claims required by Supabase. You can include additional claims or modify them as necessary. [Shortcodes](https://docs.clerk.dev/popular-guides/jwt-templates#shortcodes) are also available for adding dynamic values.
|
||||
This will pre-populate the default claims required by Supabase. You can include additional claims or modify them as necessary. [Shortcodes](https://clerk.com/docs/request-authentication/jwt-templates#shortcodes) are also available for adding dynamic values.
|
||||
|
||||
ℹ️ Note the name of the JWT template (which you can change) because this will be needed later.
|
||||
|
||||
@@ -59,7 +59,7 @@ NEXT_PUBLIC_SUPABASE_KEY=your-supabase-anon-key
|
||||
|
||||
**Note**: If you're using Create React App, replace the `NEXT_PUBLIC` prefix with `REACT_APP`
|
||||
|
||||
Your Clerk Frontend API can be found on the [API Keys](https://dashboard.clerk.dev/last-active?path=api-keys) screen.
|
||||
Your Clerk Frontend API can be found on the [API Keys](https://dashboard.clerk.com/last-active?path=api-keys) screen.
|
||||
|
||||

|
||||
|
||||
@@ -182,8 +182,8 @@ If you would like the requesting user ID from the JWT to automatically populate
|
||||
## Resources
|
||||
|
||||
- [Clerk + Supabase starter repo](https://github.com/clerkinc/clerk-supabase-starter)
|
||||
- [Next.js + Supabase + Clerk tutorial](https://clerk.dev/blog/nextjs-supabase-todos-with-multifactor-authentication)
|
||||
- [Clerk guide for Next.js Authentication](https://docs.clerk.dev/get-started/nextjs)
|
||||
- [Next.js + Supabase + Clerk tutorial](https://clerk.com/blog/nextjs-supabase-todos-with-multifactor-authentication)
|
||||
- [Clerk guide for Next.js Authentication](https://clerk.com/docs/nextjs/get-started-with-nextjs)
|
||||
- [Clerk Community Discord channel](https://discord.com/invite/b5rXHjAg7A)
|
||||
|
||||
export const Page = ({ children }) => <Layout meta={meta} children={children} />
|
||||
|
||||
@@ -36,6 +36,12 @@ The project the request was being made against has been paused. The project cann
|
||||
|
||||
Free-plan projects may be paused due to inactivity, on request by the owner, or in rare instances, due to abuse.
|
||||
|
||||
#### 544 Project API Gateway Timeout
|
||||
|
||||
The request is not completed within the configured time limit.
|
||||
|
||||
The timeout limit is set to prevent long-running queries which can cause performance issues, increase latency, and potentially even crash the project.
|
||||
|
||||
export const Page = ({ children }) => <Layout meta={meta} children={children} />
|
||||
|
||||
export default Page
|
||||
@@ -0,0 +1,5 @@
|
||||
<svg viewBox="0 0 16 16" width="16" height="16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
d="M7.99886 7.63216V14.4892M7.99886 7.63216L14.0488 4.11804M7.99886 7.63216L1.94922 4.11819M1.94922 4.11819V8.32332M1.94922 4.11819V4.08217L5.57319 1.97717M14.049 8.36007V4.08217L10.4251 1.97717M11.8165 12.4072L7.99913 14.6245L4.18177 12.4072"
|
||||
stroke="#299764" strokeMiterlimit="10" strokeLinecap="round" strokeLinejoin="round" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 449 B |
@@ -0,0 +1,5 @@
|
||||
<svg viewBox="0 0 16 16" width="16" height="16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
d="M7.99886 7.63216V14.4892M7.99886 7.63216L14.0488 4.11804M7.99886 7.63216L1.94922 4.11819M1.94922 4.11819V8.32332M1.94922 4.11819V4.08217L5.57319 1.97717M14.049 8.36007V4.08217L10.4251 1.97717M11.8165 12.4072L7.99913 14.6245L4.18177 12.4072"
|
||||
stroke="#4CC38A" strokeMiterlimit="10" strokeLinecap="round" strokeLinejoin="round" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 449 B |
@@ -5,13 +5,18 @@ import { Configuration, OpenAIApi } from 'openai'
|
||||
import { inspect } from 'util'
|
||||
import { v4 as uuidv4 } from 'uuid'
|
||||
import { fetchSources } from './sources'
|
||||
import yargs from 'yargs'
|
||||
|
||||
dotenv.config()
|
||||
|
||||
async function generateEmbeddings() {
|
||||
// TODO: use better CLI lib like yargs
|
||||
const args = process.argv.slice(2)
|
||||
const shouldRefresh = args.includes('--refresh')
|
||||
const argv = await yargs.option('refresh', {
|
||||
alias: 'r',
|
||||
description: 'Refresh data',
|
||||
type: 'boolean',
|
||||
}).argv
|
||||
|
||||
const shouldRefresh = argv.refresh
|
||||
|
||||
const requiredEnvVars = [
|
||||
'NEXT_PUBLIC_SUPABASE_URL',
|
||||
|
||||
@@ -0,0 +1,201 @@
|
||||
---
|
||||
title: 'Supabase Vecs: a vector client for Postgres'
|
||||
description: Introducing Supabase Vecs, a PostgreSQL vector client
|
||||
author: oli_rice
|
||||
image: 2023-06-01-vecs/og-image.jpg
|
||||
thumb: 2023-06-01-vecs/og-image.jpg
|
||||
tags:
|
||||
- postgres
|
||||
- AI
|
||||
- python
|
||||
date: '2023-05-29'
|
||||
toc_depth: 3
|
||||
---
|
||||
|
||||
[Vecs](https://github.com/supabase/vecs) is a new Python library for managing embeddings in your Postgres database with the pgvector extension.
|
||||
|
||||
It handles:
|
||||
|
||||
- Creating and indexing tables
|
||||
- Querying vectors by cosine distance, l2 distance, and max inner dot product
|
||||
- Filtering based on user-defined metadata
|
||||
|
||||
## Goals
|
||||
|
||||
Our goal for `vecs` is to provide an interface that lets Postgres + pgvector look and feel like a dedicated vector store. It works with any Postgres database (or platform) that [supports pgvector](https://github.com/pgvector/pgvector/issues/54).
|
||||
It was designed with ease-of-use, interactivity, and exploratory data analysis in mind, but works equally well as a search workhorse.
|
||||
|
||||
If you're interested in the nuts and bolts of what's going on, it's trivial to drop into the SQL layer and see what's happening.
|
||||
Alternatively, folks who don't want to know what's happening in the database, don't need to care.
|
||||
|
||||
## Usage
|
||||
|
||||
`Vecs` makes it easy to create a collection (table) and insert a few records - just 5 lines of code.
|
||||
|
||||
### Connecting
|
||||
|
||||
```python
|
||||
import vecs
|
||||
|
||||
DB_CONNECTION = "postgresql://<user>:<password>@<host>:<port>/<db_name>"
|
||||
|
||||
# create vector store client
|
||||
vx = vecs.create_client(DB_CONNECTION)
|
||||
|
||||
# create a collection of vectors with 3 dimensions
|
||||
docs = vx.create_collection(name="docs", dimension=3)
|
||||
```
|
||||
|
||||
The `create_collection` call sets up a table in the Postgres database specified by `DB_CONNECTION` in a schema named `vecs` with the user defined name `docs`.
|
||||
|
||||
Or, more specifically:
|
||||
|
||||
```sql
|
||||
create table vecs.docs (
|
||||
id text primary key,
|
||||
vec vector(3) not null,
|
||||
metadata jsonb not null default '{}'::jsonb
|
||||
);
|
||||
```
|
||||
|
||||
### Insert/Update
|
||||
|
||||
We can insert a few records in that new SQL table/vecs collection using `Collection.upsert`.
|
||||
|
||||
```python
|
||||
# add records to the collection
|
||||
docs.upsert(
|
||||
vectors=[
|
||||
(
|
||||
"vec0", # the records user defined identifier
|
||||
[0.1, 0.2, 0.3], # the vector. A list or np.array
|
||||
{"year": 1973} # associated metadata
|
||||
)
|
||||
]
|
||||
)
|
||||
```
|
||||
|
||||
which will add the records to our table if the `id` `"vec0"` does not exist, or updates the existing record if it does exist.
|
||||
|
||||
### Query
|
||||
|
||||
You can query a vecs collection at any time without an index, but it's a best practices to create an index on your collection after inserting
|
||||
data.
|
||||
|
||||
```python
|
||||
docs.index()
|
||||
```
|
||||
|
||||
Where `index` optionally takes an argument for the distance measure to index.
|
||||
|
||||
Finally, we can search the collection for similar vectors using the `query` method:
|
||||
|
||||
```python
|
||||
docs.query(
|
||||
query_vector=[0.10,0.21,0.29], # required
|
||||
limit=1, # (optional) number of records to return
|
||||
filters={"year": {"$eq": 1973}}, # (optional) metadata filters
|
||||
measure="cosine_distance", # (optional) distance measure to use
|
||||
include_value=False, # (optional) should distance measure values be returned?
|
||||
include_metadata=False, # (optional) should record metadata be returned?
|
||||
)
|
||||
```
|
||||
|
||||
Which returns:
|
||||
|
||||
```python
|
||||
[("vec1", 0.000697, {"year": 1973})]
|
||||
```
|
||||
|
||||
Since all metadata is stored in a `jsonb` column, there's a [lightweight but flexible DSL](https://supabase.github.io/vecs/concepts_metadata/) wrapped around it for filtering.
|
||||
|
||||
When you're done, disconnect with:
|
||||
|
||||
```python
|
||||
vx.disconnect()
|
||||
```
|
||||
|
||||
And 90% of the time, that minimal interface is all you'll need to touch.
|
||||
|
||||
For more in-depth information about `vecs`, checkout the [API Quickstart](https://supabase.github.io/vecs/api/),
|
||||
[celebrity look-alike demo](https://github.com/supabase/supabase/blob/be1f4ea85e10cc8134e389dcdbe6a092b08612f1/examples/ai/face_similarity.ipynb),
|
||||
or [OpenAI integration example](https://supabase.github.io/vecs/integrations_openai/)
|
||||
|
||||
## Deploying with Supabase
|
||||
|
||||
As usual, if you combine [supabase/vecs](https://github.com/supabase/vecs) with the rest of Supabase, you get more than the sum of the parts.
|
||||
Once you're happy with your vecs collection, you can make it acessible to your front-end through a supabase client library by exposing the collection as a view
|
||||
in your public schema.
|
||||
|
||||

|
||||
|
||||
For example, you could create a view
|
||||
|
||||
```sql
|
||||
create view public.docs as
|
||||
select
|
||||
id,
|
||||
embedding,
|
||||
metadata, # Expose the metadata as JSON
|
||||
(metadata->>'url')::text as url # Extract the URL as a string
|
||||
from
|
||||
vecs.docs
|
||||
```
|
||||
|
||||
And then access it with the supabase-js client library within your applications:
|
||||
|
||||
```js
|
||||
const { data, error } = await supabase
|
||||
.from('docs')
|
||||
.select('id, embedding, metadata')
|
||||
.eq('url', '/hello-world')
|
||||
```
|
||||
|
||||
For more deployment options, including enterprise scalable architecture, check out the [engineering for scale guide](https://supabase.com/docs/guides/ai/engineering-for-scale#simple-workloads).
|
||||
|
||||
## Future ideas
|
||||
|
||||
Currently, `vecs` is unopinionated about where vectors come from or how they're produced. While there will always be a need for generic vector
|
||||
storage and querying, it's becoming clear that text and image vectorization make up +95% of usage. That gives us the opportunity to
|
||||
streamline those workflows for users.
|
||||
|
||||
One option we're exploring is to optionally assign transformation pipelines to collections along the lines of:
|
||||
|
||||
```python
|
||||
# This is mock code only, not currently functional
|
||||
|
||||
docs: Collection =vx.create_collection(
|
||||
docs='docs',
|
||||
dimension=512,
|
||||
transform = TextPreprocessor( # this is new
|
||||
model="sentence-transformers/all-Mini-L6-v2"
|
||||
)
|
||||
)
|
||||
|
||||
docs.upsert([
|
||||
("id_0", "# Some markdown", {}),
|
||||
("id_1", "# Some more markdown", {})
|
||||
])
|
||||
```
|
||||
|
||||
so users can choose to work with their preferred media type without ever thinking about vectors.
|
||||
|
||||
Another direction we're considering is adding an async client to avoid blocking when waiting on the database or network i.e.
|
||||
|
||||
```python
|
||||
# This is mock code only, not currently functional
|
||||
|
||||
await docs.upsert([
|
||||
("id_0", [0.1, 0.2, 0.3], {}),
|
||||
])
|
||||
```
|
||||
|
||||
Both possibilities are still up for debate. If you have view on either, feel free to weigh in on the
|
||||
[Feature Request: Preprocessing Transform](https://github.com/supabase/vecs/issues/5)
|
||||
and [Feature Request: Async Client](https://github.com/supabase/vecs/issues/6) GitHub issues.
|
||||
|
||||
## More info
|
||||
|
||||
- Source code: [github.com/supabase/vecs](https://github.com/supabase/vecs)
|
||||
- Vecs Docs: [supabase.github.io/vecs/](https://supabase.github.io/vecs/)
|
||||
- Supabase Vector Toolkit: [supabase.com/docs/guides/ai](/docs/guides/ai)
|
||||
@@ -18,8 +18,11 @@ export default {
|
||||
),
|
||||
subheader: (
|
||||
<>
|
||||
Use pgvector with Supabase client libraries <br className="hidden md:block" />
|
||||
to store, index, and query your vector embeddings at scale.
|
||||
Use the Supabase client libraries to store, index, and query your vector embeddings at
|
||||
scale.
|
||||
<br className="hidden md:block" />
|
||||
Build AI applications with your Postgres and pgvector. The best vector database is the
|
||||
database you already have.
|
||||
</>
|
||||
),
|
||||
image: '/images/product/vector/vector-hero.svg',
|
||||
|
||||
@@ -21,9 +21,9 @@ const EnterpriseCta = dynamic(() => import('~/components/Sections/EnterpriseCta'
|
||||
function VectorPage() {
|
||||
// base path for images
|
||||
const { basePath } = useRouter()
|
||||
const meta_title = 'Vector | The open source vector toolkit for Postgres'
|
||||
const meta_title = 'Supabase Vector | The open source vector toolkit for Postgres.'
|
||||
const meta_description =
|
||||
'Integrate Supabase Vector database with your favorite ML-models to store, index and access vector embeddings for any AI use case.'
|
||||
'An open source toolkit for developing AI applications using Postgres and pgvector. Integrate with your favorite ML-models to store, index, and access vector embeddings for any AI use case.'
|
||||
|
||||
return (
|
||||
<>
|
||||
|
||||
|
After Width: | Height: | Size: 131 KiB |
|
After Width: | Height: | Size: 66 KiB |
@@ -1,8 +0,0 @@
|
||||
{
|
||||
"trailingComma": "es5",
|
||||
"tabWidth": 2,
|
||||
"semi": false,
|
||||
"singleQuote": true,
|
||||
"printWidth": 100,
|
||||
"endOfLine": "lf"
|
||||
}
|
||||
@@ -1,131 +0,0 @@
|
||||
import { useState, useEffect } from 'react'
|
||||
import { useUser, useSupabaseClient, Session } from '@supabase/auth-helpers-react'
|
||||
import Avatar from './Avatar'
|
||||
|
||||
import { Database } from '../utils/database.types'
|
||||
type Profiles = Database['public']['Tables']['profiles']['Row']
|
||||
|
||||
export default function Account({ session }: { session: Session }) {
|
||||
const supabase = useSupabaseClient<Database>()
|
||||
const user = useUser()
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [username, setUsername] = useState<Profiles['username']>(null)
|
||||
const [website, setWebsite] = useState<Profiles['website']>(null)
|
||||
const [avatar_url, setAvatarUrl] = useState<Profiles['avatar_url']>(null)
|
||||
|
||||
useEffect(() => {
|
||||
async function getProfile() {
|
||||
try {
|
||||
setLoading(true)
|
||||
if (!user) throw new Error('No user')
|
||||
|
||||
let { data, error, status } = await supabase
|
||||
.from('profiles')
|
||||
.select(`username, website, avatar_url`)
|
||||
.eq('id', user.id)
|
||||
.single()
|
||||
|
||||
if (error && status !== 406) {
|
||||
throw error
|
||||
}
|
||||
|
||||
if (data) {
|
||||
setUsername(data.username)
|
||||
setWebsite(data.website)
|
||||
setAvatarUrl(data.avatar_url)
|
||||
}
|
||||
} catch (error) {
|
||||
alert('Error loading user data!')
|
||||
console.log(error)
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
getProfile()
|
||||
}, [session, user, supabase])
|
||||
|
||||
async function updateProfile({
|
||||
username,
|
||||
website,
|
||||
avatar_url,
|
||||
}: {
|
||||
username: Profiles['username']
|
||||
website: Profiles['website']
|
||||
avatar_url: Profiles['avatar_url']
|
||||
}) {
|
||||
try {
|
||||
setLoading(true)
|
||||
if (!user) throw new Error('No user')
|
||||
|
||||
const updates = {
|
||||
id: user.id,
|
||||
username,
|
||||
website,
|
||||
avatar_url,
|
||||
updated_at: new Date().toISOString(),
|
||||
}
|
||||
|
||||
let { error } = await supabase.from('profiles').upsert(updates)
|
||||
if (error) throw error
|
||||
alert('Profile updated!')
|
||||
} catch (error) {
|
||||
alert('Error updating the data!')
|
||||
console.log(error)
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="form-widget">
|
||||
<Avatar
|
||||
uid={user!.id}
|
||||
url={avatar_url}
|
||||
size={150}
|
||||
onUpload={(url) => {
|
||||
setAvatarUrl(url)
|
||||
updateProfile({ username, website, avatar_url: url })
|
||||
}}
|
||||
/>
|
||||
<div>
|
||||
<label htmlFor="email">Email</label>
|
||||
<input id="email" type="text" value={session.user.email} disabled />
|
||||
</div>
|
||||
<div>
|
||||
<label htmlFor="username">Username</label>
|
||||
<input
|
||||
id="username"
|
||||
type="text"
|
||||
value={username || ''}
|
||||
onChange={(e) => setUsername(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label htmlFor="website">Website</label>
|
||||
<input
|
||||
id="website"
|
||||
type="url"
|
||||
value={website || ''}
|
||||
onChange={(e) => setWebsite(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<button
|
||||
className="button primary block"
|
||||
onClick={() => updateProfile({ username, website, avatar_url })}
|
||||
disabled={loading}
|
||||
>
|
||||
{loading ? 'Loading ...' : 'Update'}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<button className="button block" onClick={() => supabase.auth.signOut()}>
|
||||
Sign Out
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,20 +0,0 @@
|
||||
export default function Footer() {
|
||||
return (
|
||||
<div className="footer">
|
||||
<div className=""></div>
|
||||
<div className="">
|
||||
<span className="text-sm">Powered by</span>
|
||||
<img src="logo.png" />
|
||||
</div>
|
||||
<div className="flex-end flex">
|
||||
<a
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
href="https://vercel.com/new/git/external?repository-url=https%3A%2F%2Fgithub.com%2Fsupabase%2Fsupabase%2Ftree%2Fmaster%2Fexamples%2Fuser-management%2Fnextjs-ts-user-management&project-name=supabase-user-management&repository-name=supabase-user-management&demo-title=Supabase%20User%20Management&demo-description=An%20example%20web%20app%20using%20Supabase%20and%20Next.js&demo-url=https%3A%2F%2Fsupabase-nextjs-ts-user-management.vercel.app&demo-image=https%3A%2F%2Fi.imgur.com%2FZ3HkQqe.png&integration-ids=oac_jUduyjQgOyzev1fjrW83NYOv&external-id=nextjs-user-management"
|
||||
>
|
||||
<img src="https://vercel.com/button" alt="vercel button" />
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,5 +0,0 @@
|
||||
/// <reference types="next" />
|
||||
/// <reference types="next/image-types/global" />
|
||||
|
||||
// NOTE: This file should not be edited
|
||||
// see https://nextjs.org/docs/basic-features/typescript for more information.
|
||||
@@ -1,27 +0,0 @@
|
||||
{
|
||||
"name": "nextjs-ts-user-management",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "next dev",
|
||||
"build": "next build",
|
||||
"start": "next start"
|
||||
},
|
||||
"dependencies": {
|
||||
"@supabase/auth-helpers-nextjs": "^0.5.4",
|
||||
"@supabase/auth-helpers-react": "^0.3.1",
|
||||
"@supabase/auth-ui-react": "^0.2.6",
|
||||
"@supabase/supabase-js": "^2.5.0",
|
||||
"next": "12.3.1",
|
||||
"react": "18.2.0",
|
||||
"react-dom": "18.2.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "18.8.5",
|
||||
"@types/react": "18.0.21",
|
||||
"@types/react-dom": "18.0.6",
|
||||
"eslint": "8.25.0",
|
||||
"eslint-config-next": "12.3.1",
|
||||
"typescript": "4.8.4"
|
||||
}
|
||||
}
|
||||
@@ -1,25 +0,0 @@
|
||||
import '../styles/globals.css'
|
||||
import { useState } from 'react'
|
||||
import type { AppProps } from 'next/app'
|
||||
import { createBrowserSupabaseClient } from '@supabase/auth-helpers-nextjs'
|
||||
import { SessionContextProvider, Session } from '@supabase/auth-helpers-react'
|
||||
|
||||
function MyApp({
|
||||
Component,
|
||||
pageProps,
|
||||
}: AppProps<{
|
||||
initialSession: Session
|
||||
}>) {
|
||||
const [supabaseClient] = useState(() => createBrowserSupabaseClient())
|
||||
|
||||
return (
|
||||
<SessionContextProvider
|
||||
supabaseClient={supabaseClient}
|
||||
initialSession={pageProps.initialSession}
|
||||
>
|
||||
<Component {...pageProps} />
|
||||
</SessionContextProvider>
|
||||
)
|
||||
}
|
||||
|
||||
export default MyApp
|
||||
@@ -1,38 +0,0 @@
|
||||
import type { NextPage } from 'next'
|
||||
import { Auth, ThemeSupa } from '@supabase/auth-ui-react'
|
||||
import { useSession, useSupabaseClient } from '@supabase/auth-helpers-react'
|
||||
import Account from '../components/Account'
|
||||
import Footer from '../components/Footer'
|
||||
|
||||
const Home: NextPage = () => {
|
||||
const session = useSession()
|
||||
const supabase = useSupabaseClient()
|
||||
|
||||
return (
|
||||
<div className="container" style={{ padding: '50px 0 100px 0' }}>
|
||||
{!session ? (
|
||||
<div className="row">
|
||||
<div className="col-6">
|
||||
<h1 className="header">Supabase Auth + Storage</h1>
|
||||
<p className="">
|
||||
Experience our Auth and Storage through a simple profile management example. Create a
|
||||
user profile and upload an avatar image. Fast, simple, secure.
|
||||
</p>
|
||||
</div>
|
||||
<div className="col-6 auth-widget">
|
||||
<Auth supabaseClient={supabase} appearance={{ theme: ThemeSupa }} theme="dark" />
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
<h3>Account</h3>
|
||||
<Account session={session} />
|
||||
</>
|
||||
)}
|
||||
|
||||
<Footer />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default Home
|
||||
|
Before Width: | Height: | Size: 15 KiB |
@@ -1 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-loader"><line x1="12" y1="2" x2="12" y2="6"></line><line x1="12" y1="18" x2="12" y2="22"></line><line x1="4.93" y1="4.93" x2="7.76" y2="7.76"></line><line x1="16.24" y1="16.24" x2="19.07" y2="19.07"></line><line x1="2" y1="12" x2="6" y2="12"></line><line x1="18" y1="12" x2="22" y2="12"></line><line x1="4.93" y1="19.07" x2="7.76" y2="16.24"></line><line x1="16.24" y1="7.76" x2="19.07" y2="4.93"></line></svg>
|
||||
|
Before Width: | Height: | Size: 614 B |
|
Before Width: | Height: | Size: 10 KiB |
@@ -1,4 +0,0 @@
|
||||
<svg width="283" height="64" viewBox="0 0 283 64" fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M141.04 16c-11.04 0-19 7.2-19 18s8.96 18 20 18c6.67 0 12.55-2.64 16.19-7.09l-7.65-4.42c-2.02 2.21-5.09 3.5-8.54 3.5-4.79 0-8.86-2.5-10.37-6.5h28.02c.22-1.12.35-2.28.35-3.5 0-10.79-7.96-17.99-19-17.99zm-9.46 14.5c1.25-3.99 4.67-6.5 9.45-6.5 4.79 0 8.21 2.51 9.45 6.5h-18.9zM248.72 16c-11.04 0-19 7.2-19 18s8.96 18 20 18c6.67 0 12.55-2.64 16.19-7.09l-7.65-4.42c-2.02 2.21-5.09 3.5-8.54 3.5-4.79 0-8.86-2.5-10.37-6.5h28.02c.22-1.12.35-2.28.35-3.5 0-10.79-7.96-17.99-19-17.99zm-9.45 14.5c1.25-3.99 4.67-6.5 9.45-6.5 4.79 0 8.21 2.51 9.45 6.5h-18.9zM200.24 34c0 6 3.92 10 10 10 4.12 0 7.21-1.87 8.8-4.92l7.68 4.43c-3.18 5.3-9.14 8.49-16.48 8.49-11.05 0-19-7.2-19-18s7.96-18 19-18c7.34 0 13.29 3.19 16.48 8.49l-7.68 4.43c-1.59-3.05-4.68-4.92-8.8-4.92-6.07 0-10 4-10 10zm82.48-29v46h-9V5h9zM36.95 0L73.9 64H0L36.95 0zm92.38 5l-27.71 48L73.91 5H84.3l17.32 30 17.32-30h10.39zm58.91 12v9.69c-1-.29-2.06-.49-3.2-.49-5.81 0-10 4-10 10V51h-9V17h9v9.2c0-5.08 5.91-9.2 13.2-9.2z" fill="#000"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 1.1 KiB |
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"extends": "next/core-web-vitals"
|
||||
}
|
||||
@@ -25,10 +25,11 @@ yarn-debug.log*
|
||||
yarn-error.log*
|
||||
|
||||
# local env files
|
||||
.env.local
|
||||
.env.development.local
|
||||
.env.test.local
|
||||
.env.production.local
|
||||
.env*.local
|
||||
|
||||
# vercel
|
||||
.vercel
|
||||
|
||||
# typescript
|
||||
*.tsbuildinfo
|
||||
next-env.d.ts
|
||||
@@ -0,0 +1,139 @@
|
||||
'use client'
|
||||
import { useCallback, useEffect, useState } from 'react'
|
||||
import Avatar from './avatar'
|
||||
import { Database } from '../database.types'
|
||||
import { Session, createClientComponentClient } from '@supabase/auth-helpers-nextjs'
|
||||
|
||||
export default function AccountForm({ session }: { session: Session | null }) {
|
||||
const supabase = createClientComponentClient<Database>()
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [fullname, setFullname] = useState<string | null>(null)
|
||||
const [username, setUsername] = useState<string | null>(null)
|
||||
const [website, setWebsite] = useState<string | null>(null)
|
||||
const [avatar_url, setAvatarUrl] = useState<string | null>(null)
|
||||
const user = session?.user
|
||||
|
||||
const getProfile = useCallback(async () => {
|
||||
try {
|
||||
setLoading(true)
|
||||
|
||||
let { data, error, status } = await supabase
|
||||
.from('profiles')
|
||||
.select(`full_name, username, website, avatar_url`)
|
||||
.eq('id', user?.id)
|
||||
.single()
|
||||
|
||||
if (error && status !== 406) {
|
||||
throw error
|
||||
}
|
||||
|
||||
if (data) {
|
||||
setFullname(data.full_name)
|
||||
setUsername(data.username)
|
||||
setWebsite(data.website)
|
||||
setAvatarUrl(data.avatar_url)
|
||||
}
|
||||
} catch (error) {
|
||||
alert('Error loading user data!')
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}, [user, supabase])
|
||||
|
||||
useEffect(() => {
|
||||
getProfile()
|
||||
}, [user, getProfile])
|
||||
|
||||
async function updateProfile({
|
||||
username,
|
||||
website,
|
||||
avatar_url,
|
||||
}: {
|
||||
username: string | null
|
||||
fullname: string | null
|
||||
website: string | null
|
||||
avatar_url: string | null
|
||||
}) {
|
||||
try {
|
||||
setLoading(true)
|
||||
|
||||
let { error } = await supabase.from('profiles').upsert({
|
||||
id: user?.id as string,
|
||||
full_name: fullname,
|
||||
username,
|
||||
website,
|
||||
avatar_url,
|
||||
updated_at: new Date().toISOString(),
|
||||
})
|
||||
if (error) throw error
|
||||
alert('Profile updated!')
|
||||
} catch (error) {
|
||||
alert('Error updating the data!')
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="form-widget">
|
||||
<Avatar
|
||||
uid={user!.id}
|
||||
url={avatar_url}
|
||||
size={150}
|
||||
onUpload={(url) => {
|
||||
setAvatarUrl(url)
|
||||
updateProfile({ fullname, username, website, avatar_url: url })
|
||||
}}
|
||||
/>
|
||||
<div>
|
||||
<label htmlFor="email">Email</label>
|
||||
<input id="email" type="text" value={session?.user.email} disabled />
|
||||
</div>
|
||||
<div>
|
||||
<label htmlFor="fullName">Full Name</label>
|
||||
<input
|
||||
id="fullName"
|
||||
type="text"
|
||||
value={fullname || ''}
|
||||
onChange={(e) => setFullname(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label htmlFor="username">Username</label>
|
||||
<input
|
||||
id="username"
|
||||
type="text"
|
||||
value={username || ''}
|
||||
onChange={(e) => setUsername(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label htmlFor="website">Website</label>
|
||||
<input
|
||||
id="website"
|
||||
type="url"
|
||||
value={website || ''}
|
||||
onChange={(e) => setWebsite(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<button
|
||||
className="button primary block"
|
||||
onClick={() => updateProfile({ fullname, username, website, avatar_url })}
|
||||
disabled={loading}
|
||||
>
|
||||
{loading ? 'Loading ...' : 'Update'}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<form action="/auth/signout" method="post">
|
||||
<button className="button block" type="submit">
|
||||
Sign out
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,6 +1,8 @@
|
||||
'use client'
|
||||
import React, { useEffect, useState } from 'react'
|
||||
import { useSupabaseClient } from '@supabase/auth-helpers-react'
|
||||
import { Database } from '../utils/database.types'
|
||||
import { Database } from '../database.types'
|
||||
import { createClientComponentClient } from '@supabase/auth-helpers-nextjs'
|
||||
import Image from 'next/image'
|
||||
type Profiles = Database['public']['Tables']['profiles']['Row']
|
||||
|
||||
export default function Avatar({
|
||||
@@ -14,19 +16,13 @@ export default function Avatar({
|
||||
size: number
|
||||
onUpload: (url: string) => void
|
||||
}) {
|
||||
const supabase = useSupabaseClient<Database>()
|
||||
const [avatarUrl, setAvatarUrl] = useState<Profiles['avatar_url']>(null)
|
||||
const supabase = createClientComponentClient<Database>()
|
||||
const [avatarUrl, setAvatarUrl] = useState<Profiles['avatar_url']>(url)
|
||||
const [uploading, setUploading] = useState(false)
|
||||
const [blobUrlCache, setBlobUrlCache] = useState<Record<string, string>>({})
|
||||
|
||||
useEffect(() => {
|
||||
async function downloadImage(path: string) {
|
||||
try {
|
||||
if (blobUrlCache[path]) {
|
||||
setAvatarUrl(blobUrlCache[path])
|
||||
return
|
||||
}
|
||||
|
||||
const { data, error } = await supabase.storage.from('avatars').download(path)
|
||||
if (error) {
|
||||
throw error
|
||||
@@ -34,14 +30,13 @@ export default function Avatar({
|
||||
|
||||
const url = URL.createObjectURL(data)
|
||||
setAvatarUrl(url)
|
||||
setBlobUrlCache((prevCache) => ({ ...prevCache, [path]: url }))
|
||||
} catch (error) {
|
||||
console.log('Error downloading image: ', error)
|
||||
}
|
||||
}
|
||||
|
||||
if (url) downloadImage(url)
|
||||
}, [url, supabase.storage, blobUrlCache])
|
||||
}, [url, supabase])
|
||||
|
||||
const uploadAvatar: React.ChangeEventHandler<HTMLInputElement> = async (event) => {
|
||||
try {
|
||||
@@ -53,12 +48,9 @@ export default function Avatar({
|
||||
|
||||
const file = event.target.files[0]
|
||||
const fileExt = file.name.split('.').pop()
|
||||
const fileName = `${uid}.${fileExt}`
|
||||
const filePath = `${fileName}`
|
||||
const filePath = `${uid}-${Math.random()}.${fileExt}`
|
||||
|
||||
let { error: uploadError } = await supabase.storage
|
||||
.from('avatars')
|
||||
.upload(filePath, file, { upsert: true })
|
||||
let { error: uploadError } = await supabase.storage.from('avatars').upload(filePath, file)
|
||||
|
||||
if (uploadError) {
|
||||
throw uploadError
|
||||
@@ -67,7 +59,6 @@ export default function Avatar({
|
||||
onUpload(filePath)
|
||||
} catch (error) {
|
||||
alert('Error uploading avatar!')
|
||||
console.log(error)
|
||||
} finally {
|
||||
setUploading(false)
|
||||
}
|
||||
@@ -76,7 +67,9 @@ export default function Avatar({
|
||||
return (
|
||||
<div>
|
||||
{avatarUrl ? (
|
||||
<img
|
||||
<Image
|
||||
width={size}
|
||||
height={size}
|
||||
src={avatarUrl}
|
||||
alt="Avatar"
|
||||
className="avatar image"
|
||||
@@ -0,0 +1,14 @@
|
||||
import { createServerComponentClient } from '@supabase/auth-helpers-nextjs'
|
||||
import { cookies } from 'next/headers'
|
||||
import { Database } from '../database.types'
|
||||
import AccountForm from './account-form'
|
||||
|
||||
export default async function Account() {
|
||||
const supabase = createServerComponentClient<Database>({ cookies })
|
||||
|
||||
const {
|
||||
data: { session },
|
||||
} = await supabase.auth.getSession()
|
||||
|
||||
return <AccountForm session={session} />
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
'use client'
|
||||
import { Auth } from '@supabase/auth-ui-react'
|
||||
import { ThemeSupa } from '@supabase/auth-ui-shared'
|
||||
import { createClientComponentClient } from '@supabase/auth-helpers-nextjs'
|
||||
import { Database } from './database.types'
|
||||
|
||||
export default function AuthForm() {
|
||||
const supabase = createClientComponentClient<Database>()
|
||||
return (
|
||||
<Auth
|
||||
supabaseClient={supabase}
|
||||
view="magic_link"
|
||||
appearance={{ theme: ThemeSupa }}
|
||||
theme="dark"
|
||||
showLinks={false}
|
||||
providers={[]}
|
||||
redirectTo="http://localhost:3000/auth/callback"
|
||||
/>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
import { createRouteHandlerClient } from '@supabase/auth-helpers-nextjs'
|
||||
import { cookies } from 'next/headers'
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
|
||||
export async function GET(req: NextRequest) {
|
||||
const supabase = createRouteHandlerClient({ cookies })
|
||||
const { searchParams } = new URL(req.url)
|
||||
const code = searchParams.get('code')
|
||||
|
||||
if (code) {
|
||||
await supabase.auth.exchangeCodeForSession(code)
|
||||
}
|
||||
|
||||
return NextResponse.redirect(new URL('/account', req.url))
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
import { createRouteHandlerClient } from '@supabase/auth-helpers-nextjs'
|
||||
import { cookies } from 'next/headers'
|
||||
import { type NextRequest, NextResponse } from 'next/server'
|
||||
|
||||
export async function POST(req: NextRequest) {
|
||||
const supabase = createRouteHandlerClient({ cookies })
|
||||
|
||||
// Check if we have a session
|
||||
const {
|
||||
data: { session },
|
||||
} = await supabase.auth.getSession()
|
||||
|
||||
if (session) {
|
||||
await supabase.auth.signOut()
|
||||
}
|
||||
|
||||
return NextResponse.redirect(new URL('/', req.url), {
|
||||
status: 302,
|
||||
})
|
||||
}
|
||||
|
After Width: | Height: | Size: 25 KiB |
@@ -1,7 +1,7 @@
|
||||
html,
|
||||
body {
|
||||
--custom-font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Oxygen, Ubuntu,
|
||||
Cantarell, Fira Sans, Droid Sans, Helvetica Neue, sans-serif;
|
||||
--custom-font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Roboto,
|
||||
Oxygen, Ubuntu, Cantarell, Fira Sans, Droid Sans, Helvetica Neue, sans-serif;
|
||||
--custom-bg-color: #101010;
|
||||
--custom-panel-color: #222;
|
||||
--custom-box-shadow: 0 2px 8px 0 rgba(0, 0, 0, 0.8);
|
||||
@@ -43,7 +43,7 @@ body,
|
||||
position: relative;
|
||||
width: 100%;
|
||||
}
|
||||
.row [class^='col'] {
|
||||
.row [class^="col"] {
|
||||
float: left;
|
||||
margin: 0.5rem 2%;
|
||||
min-height: 0.125rem;
|
||||
@@ -99,7 +99,7 @@ body,
|
||||
width: 96%;
|
||||
}
|
||||
.row::after {
|
||||
content: '';
|
||||
content: "";
|
||||
display: table;
|
||||
clear: both;
|
||||
}
|
||||
@@ -345,15 +345,13 @@ button.primary,
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
/* Auth */
|
||||
|
||||
.auth-widget {
|
||||
.form-widget {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.auth-widget > .button {
|
||||
.form-widget > .button {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
@@ -363,71 +361,12 @@ button.primary,
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.auth-widget .button:hover {
|
||||
.form-widget .button:hover {
|
||||
background-color: #2a2a2a;
|
||||
}
|
||||
|
||||
.auth-widget .button > .loader,
|
||||
.account .button > .loader {
|
||||
.form-widget .button > .loader {
|
||||
width: 17px;
|
||||
animation: spin 1s linear infinite;
|
||||
filter: invert(1);
|
||||
}
|
||||
|
||||
/* Account */
|
||||
|
||||
.account {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.account > * > .avatarField {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
.account > * > .avatarField > .avatarContainer {
|
||||
margin-right: 20px;
|
||||
}
|
||||
|
||||
/* Profile Card */
|
||||
|
||||
.profileCard {
|
||||
border-radius: 5px;
|
||||
display: flex;
|
||||
border: var(--custom-border);
|
||||
background-color: var(--custom-panel-color);
|
||||
padding: 20px 20px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.profileCard:last-child {
|
||||
margin-bottom: 0px;
|
||||
}
|
||||
|
||||
.profileCard > .userInfo {
|
||||
margin-left: 20px;
|
||||
font-weight: 300;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.profileCard > .userInfo > p {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.profileCard > .userInfo > .username {
|
||||
font-size: 1.3rem;
|
||||
font-weight: 500;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
.profileCard > .userInfo > .website {
|
||||
font-size: 0.9rem;
|
||||
color: var(--custom-color-brand);
|
||||
margin-bottom: 10px;
|
||||
text-decoration: none;
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
import './globals.css'
|
||||
|
||||
export const metadata = {
|
||||
title: 'User Management',
|
||||
description: 'Generated by create next app',
|
||||
}
|
||||
|
||||
export default async function RootLayout({ children }: { children: React.ReactNode }) {
|
||||
return (
|
||||
<html lang="en">
|
||||
<body>
|
||||
<div className="container" style={{ padding: '50px 0 100px 0' }}>
|
||||
{children}
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,229 @@
|
||||
.main {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 6rem;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
.description {
|
||||
display: inherit;
|
||||
justify-content: inherit;
|
||||
align-items: inherit;
|
||||
font-size: 0.85rem;
|
||||
max-width: var(--max-width);
|
||||
width: 100%;
|
||||
z-index: 2;
|
||||
font-family: var(--font-mono);
|
||||
}
|
||||
|
||||
.description a {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.description p {
|
||||
position: relative;
|
||||
margin: 0;
|
||||
padding: 1rem;
|
||||
background-color: rgba(var(--callout-rgb), 0.5);
|
||||
border: 1px solid rgba(var(--callout-border-rgb), 0.3);
|
||||
border-radius: var(--border-radius);
|
||||
}
|
||||
|
||||
.code {
|
||||
font-weight: 700;
|
||||
font-family: var(--font-mono);
|
||||
}
|
||||
|
||||
.grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, minmax(25%, auto));
|
||||
width: var(--max-width);
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.card {
|
||||
padding: 1rem 1.2rem;
|
||||
border-radius: var(--border-radius);
|
||||
background: rgba(var(--card-rgb), 0);
|
||||
border: 1px solid rgba(var(--card-border-rgb), 0);
|
||||
transition: background 200ms, border 200ms;
|
||||
}
|
||||
|
||||
.card span {
|
||||
display: inline-block;
|
||||
transition: transform 200ms;
|
||||
}
|
||||
|
||||
.card h2 {
|
||||
font-weight: 600;
|
||||
margin-bottom: 0.7rem;
|
||||
}
|
||||
|
||||
.card p {
|
||||
margin: 0;
|
||||
opacity: 0.6;
|
||||
font-size: 0.9rem;
|
||||
line-height: 1.5;
|
||||
max-width: 30ch;
|
||||
}
|
||||
|
||||
.center {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
position: relative;
|
||||
padding: 4rem 0;
|
||||
}
|
||||
|
||||
.center::before {
|
||||
background: var(--secondary-glow);
|
||||
border-radius: 50%;
|
||||
width: 480px;
|
||||
height: 360px;
|
||||
margin-left: -400px;
|
||||
}
|
||||
|
||||
.center::after {
|
||||
background: var(--primary-glow);
|
||||
width: 240px;
|
||||
height: 180px;
|
||||
z-index: -1;
|
||||
}
|
||||
|
||||
.center::before,
|
||||
.center::after {
|
||||
content: '';
|
||||
left: 50%;
|
||||
position: absolute;
|
||||
filter: blur(45px);
|
||||
transform: translateZ(0);
|
||||
}
|
||||
|
||||
.logo {
|
||||
position: relative;
|
||||
}
|
||||
/* Enable hover only on non-touch devices */
|
||||
@media (hover: hover) and (pointer: fine) {
|
||||
.card:hover {
|
||||
background: rgba(var(--card-rgb), 0.1);
|
||||
border: 1px solid rgba(var(--card-border-rgb), 0.15);
|
||||
}
|
||||
|
||||
.card:hover span {
|
||||
transform: translateX(4px);
|
||||
}
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion) {
|
||||
.card:hover span {
|
||||
transform: none;
|
||||
}
|
||||
}
|
||||
|
||||
/* Mobile */
|
||||
@media (max-width: 700px) {
|
||||
.content {
|
||||
padding: 4rem;
|
||||
}
|
||||
|
||||
.grid {
|
||||
grid-template-columns: 1fr;
|
||||
margin-bottom: 120px;
|
||||
max-width: 320px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.card {
|
||||
padding: 1rem 2.5rem;
|
||||
}
|
||||
|
||||
.card h2 {
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.center {
|
||||
padding: 8rem 0 6rem;
|
||||
}
|
||||
|
||||
.center::before {
|
||||
transform: none;
|
||||
height: 300px;
|
||||
}
|
||||
|
||||
.description {
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
|
||||
.description a {
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.description p,
|
||||
.description div {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
position: fixed;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.description p {
|
||||
align-items: center;
|
||||
inset: 0 0 auto;
|
||||
padding: 2rem 1rem 1.4rem;
|
||||
border-radius: 0;
|
||||
border: none;
|
||||
border-bottom: 1px solid rgba(var(--callout-border-rgb), 0.25);
|
||||
background: linear-gradient(
|
||||
to bottom,
|
||||
rgba(var(--background-start-rgb), 1),
|
||||
rgba(var(--callout-rgb), 0.5)
|
||||
);
|
||||
background-clip: padding-box;
|
||||
backdrop-filter: blur(24px);
|
||||
}
|
||||
|
||||
.description div {
|
||||
align-items: flex-end;
|
||||
pointer-events: none;
|
||||
inset: auto 0 0;
|
||||
padding: 2rem;
|
||||
height: 200px;
|
||||
background: linear-gradient(
|
||||
to bottom,
|
||||
transparent 0%,
|
||||
rgb(var(--background-end-rgb)) 40%
|
||||
);
|
||||
z-index: 1;
|
||||
}
|
||||
}
|
||||
|
||||
/* Tablet and Smaller Desktop */
|
||||
@media (min-width: 701px) and (max-width: 1120px) {
|
||||
.grid {
|
||||
grid-template-columns: repeat(2, 50%);
|
||||
}
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
.vercelLogo {
|
||||
filter: invert(1);
|
||||
}
|
||||
|
||||
.logo {
|
||||
filter: invert(1) drop-shadow(0 0 0.3rem #ffffff70);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes rotate {
|
||||
from {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
to {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
import AuthForm from './auth-form'
|
||||
|
||||
export default function Home() {
|
||||
return (
|
||||
<div className="row">
|
||||
<div className="col-6">
|
||||
<h1 className="header">Supabase Auth + Storage</h1>
|
||||
<p className="">
|
||||
Experience our Auth and Storage through a simple profile management example. Create a user
|
||||
profile and upload an avatar image. Fast, simple, secure.
|
||||
</p>
|
||||
</div>
|
||||
<div className="col-6 auth-widget">
|
||||
<AuthForm />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
'use client'
|
||||
|
||||
import { createContext, useContext, useEffect, useState } from 'react'
|
||||
import { Session, SupabaseClient, createClientComponentClient } from '@supabase/auth-helpers-nextjs'
|
||||
import { useRouter } from 'next/navigation'
|
||||
import { Database } from './database.types'
|
||||
|
||||
type MaybeSession = Session | null
|
||||
|
||||
type SupabaseContext = {
|
||||
supabase: SupabaseClient<any, string>
|
||||
session: MaybeSession
|
||||
}
|
||||
|
||||
const Context = createContext<SupabaseContext | undefined>(undefined)
|
||||
|
||||
export default function SupabaseProvider({
|
||||
children,
|
||||
session,
|
||||
}: {
|
||||
children: React.ReactNode
|
||||
session: MaybeSession
|
||||
}) {
|
||||
const supabase = createClientComponentClient<Database>()
|
||||
const router = useRouter()
|
||||
|
||||
useEffect(() => {
|
||||
const {
|
||||
data: { subscription },
|
||||
} = supabase.auth.onAuthStateChange((_, _session) => {
|
||||
if (_session?.access_token !== session?.access_token) {
|
||||
router.refresh()
|
||||
}
|
||||
})
|
||||
|
||||
return () => {
|
||||
subscription.unsubscribe()
|
||||
}
|
||||
}, [router, supabase, session])
|
||||
|
||||
return (
|
||||
<Context.Provider value={{ supabase, session }}>
|
||||
<>{children}</>
|
||||
</Context.Provider>
|
||||
)
|
||||
}
|
||||
|
||||
export const useSupabase = <
|
||||
Database = any,
|
||||
SchemaName extends string & keyof Database = 'public' extends keyof Database
|
||||
? 'public'
|
||||
: string & keyof Database
|
||||
>() => {
|
||||
let context = useContext(Context)
|
||||
|
||||
if (context === undefined) {
|
||||
throw new Error('useSupabase must be used inside SupabaseProvider')
|
||||
}
|
||||
|
||||
return context.supabase as SupabaseClient<Database, SchemaName>
|
||||
}
|
||||
|
||||
export const useSession = () => {
|
||||
let context = useContext(Context)
|
||||
|
||||
if (context === undefined) {
|
||||
throw new Error('useSession must be used inside SupabaseProvider')
|
||||
}
|
||||
|
||||
return context.session
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
import { createMiddlewareClient } from '@supabase/auth-helpers-nextjs'
|
||||
import { NextResponse } from 'next/server'
|
||||
|
||||
import type { NextRequest } from 'next/server'
|
||||
|
||||
export async function middleware(req: NextRequest) {
|
||||
const res = NextResponse.next()
|
||||
const supabase = createMiddlewareClient({ req, res })
|
||||
|
||||
const {
|
||||
data: { user },
|
||||
} = await supabase.auth.getUser()
|
||||
|
||||
// if user is signed in and the current path is / redirect the user to /account
|
||||
if (user && req.nextUrl.pathname === '/') {
|
||||
return NextResponse.redirect(new URL('/account', req.url))
|
||||
}
|
||||
|
||||
// if user is not signed in and the current path is not / redirect the user to /
|
||||
if (!user && req.nextUrl.pathname !== '/') {
|
||||
return NextResponse.redirect(new URL('/', req.url))
|
||||
}
|
||||
|
||||
return res
|
||||
}
|
||||
|
||||
export const config = {
|
||||
matcher: ['/', '/account'],
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
/** @type {import('next').NextConfig} */
|
||||
const nextConfig = {}
|
||||
|
||||
module.exports = nextConfig
|
||||
@@ -0,0 +1,26 @@
|
||||
{
|
||||
"name": "nextjs-user-management",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "next dev",
|
||||
"build": "next build",
|
||||
"start": "next start",
|
||||
"lint": "next lint"
|
||||
},
|
||||
"dependencies": {
|
||||
"@supabase/auth-helpers-nextjs": "^0.7.1",
|
||||
"@supabase/auth-ui-react": "^0.4.2",
|
||||
"@supabase/auth-ui-shared": "^0.1.6",
|
||||
"@types/node": "20.1.4",
|
||||
"@types/react": "18.2.6",
|
||||
"@types/react-dom": "18.2.4",
|
||||
"encoding": "^0.1.13",
|
||||
"eslint": "8.40.0",
|
||||
"eslint-config-next": "13.4.2",
|
||||
"next": "13.4.2",
|
||||
"react": "18.2.0",
|
||||
"react-dom": "18.2.0",
|
||||
"typescript": "5.0.4"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 394 80"><path fill="#000" d="M262 0h68.5v12.7h-27.2v66.6h-13.6V12.7H262V0ZM149 0v12.7H94v20.4h44.3v12.6H94v21h55v12.6H80.5V0h68.7zm34.3 0h-17.8l63.8 79.4h17.9l-32-39.7 32-39.6h-17.9l-23 28.6-23-28.6zm18.3 56.7-9-11-27.1 33.7h17.8l18.3-22.7z"/><path fill="#000" d="M81 79.3 17 0H0v79.3h13.6V17l50.2 62.3H81Zm252.6-.4c-1 0-1.8-.4-2.5-1s-1.1-1.6-1.1-2.6.3-1.8 1-2.5 1.6-1 2.6-1 1.8.3 2.5 1a3.4 3.4 0 0 1 .6 4.3 3.7 3.7 0 0 1-3 1.8zm23.2-33.5h6v23.3c0 2.1-.4 4-1.3 5.5a9.1 9.1 0 0 1-3.8 3.5c-1.6.8-3.5 1.3-5.7 1.3-2 0-3.7-.4-5.3-1s-2.8-1.8-3.7-3.2c-.9-1.3-1.4-3-1.4-5h6c.1.8.3 1.6.7 2.2s1 1.2 1.6 1.5c.7.4 1.5.5 2.4.5 1 0 1.8-.2 2.4-.6a4 4 0 0 0 1.6-1.8c.3-.8.5-1.8.5-3V45.5zm30.9 9.1a4.4 4.4 0 0 0-2-3.3 7.5 7.5 0 0 0-4.3-1.1c-1.3 0-2.4.2-3.3.5-.9.4-1.6 1-2 1.6a3.5 3.5 0 0 0-.3 4c.3.5.7.9 1.3 1.2l1.8 1 2 .5 3.2.8c1.3.3 2.5.7 3.7 1.2a13 13 0 0 1 3.2 1.8 8.1 8.1 0 0 1 3 6.5c0 2-.5 3.7-1.5 5.1a10 10 0 0 1-4.4 3.5c-1.8.8-4.1 1.2-6.8 1.2-2.6 0-4.9-.4-6.8-1.2-2-.8-3.4-2-4.5-3.5a10 10 0 0 1-1.7-5.6h6a5 5 0 0 0 3.5 4.6c1 .4 2.2.6 3.4.6 1.3 0 2.5-.2 3.5-.6 1-.4 1.8-1 2.4-1.7a4 4 0 0 0 .8-2.4c0-.9-.2-1.6-.7-2.2a11 11 0 0 0-2.1-1.4l-3.2-1-3.8-1c-2.8-.7-5-1.7-6.6-3.2a7.2 7.2 0 0 1-2.4-5.7 8 8 0 0 1 1.7-5 10 10 0 0 1 4.3-3.5c2-.8 4-1.2 6.4-1.2 2.3 0 4.4.4 6.2 1.2 1.8.8 3.2 2 4.3 3.4 1 1.4 1.5 3 1.5 5h-5.8z"/></svg>
|
||||
|
After Width: | Height: | Size: 1.3 KiB |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 283 64"><path fill="black" d="M141 16c-11 0-19 7-19 18s9 18 20 18c7 0 13-3 16-7l-7-5c-2 3-6 4-9 4-5 0-9-3-10-7h28v-3c0-11-8-18-19-18zm-9 15c1-4 4-7 9-7s8 3 9 7h-18zm117-15c-11 0-19 7-19 18s9 18 20 18c6 0 12-3 16-7l-8-5c-2 3-5 4-8 4-5 0-9-3-11-7h28l1-3c0-11-8-18-19-18zm-10 15c2-4 5-7 10-7s8 3 9 7h-19zm-39 3c0 6 4 10 10 10 4 0 7-2 9-5l8 5c-3 5-9 8-17 8-11 0-19-7-19-18s8-18 19-18c8 0 14 3 17 8l-8 5c-2-3-5-5-9-5-6 0-10 4-10 10zm83-29v46h-9V5h9zM37 0l37 64H0L37 0zm92 5-27 48L74 5h10l18 30 17-30h10zm59 12v10l-3-1c-6 0-10 4-10 10v15h-9V17h9v9c0-5 6-9 13-9z"/></svg>
|
||||
|
After Width: | Height: | Size: 629 B |
@@ -1,6 +1,6 @@
|
||||
# A string used to distinguish different Supabase projects on the same host. Defaults to the working
|
||||
# directory name when running `supabase init`.
|
||||
project_id = "nextjs-ts-user-management"
|
||||
project_id = "nextjs-user-management"
|
||||
|
||||
[api]
|
||||
# Port to use for the API URL.
|
||||
@@ -1,11 +1,7 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "es5",
|
||||
"lib": [
|
||||
"dom",
|
||||
"dom.iterable",
|
||||
"esnext"
|
||||
],
|
||||
"lib": ["dom", "dom.iterable", "esnext"],
|
||||
"allowJs": true,
|
||||
"skipLibCheck": true,
|
||||
"strict": true,
|
||||
@@ -17,14 +13,16 @@
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"jsx": "preserve",
|
||||
"incremental": true
|
||||
"incremental": true,
|
||||
"plugins": [
|
||||
{
|
||||
"name": "next"
|
||||
}
|
||||
],
|
||||
"paths": {
|
||||
"@/*": ["./*"]
|
||||
}
|
||||
},
|
||||
"include": [
|
||||
"next-env.d.ts",
|
||||
"**/*.ts",
|
||||
"**/*.tsx"
|
||||
],
|
||||
"exclude": [
|
||||
"node_modules"
|
||||
]
|
||||
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
|
||||
"exclude": ["node_modules"]
|
||||
}
|
||||
@@ -8,25 +8,25 @@
|
||||
"name": "sveltekit-user-management",
|
||||
"version": "0.0.1",
|
||||
"dependencies": {
|
||||
"@supabase/auth-helpers-sveltekit": "^0.9.4",
|
||||
"@supabase/auth-ui-shared": "^0.1.3",
|
||||
"@supabase/auth-ui-svelte": "^0.1.5",
|
||||
"@supabase/auth-helpers-sveltekit": "^0.10.0",
|
||||
"@supabase/auth-ui-shared": "^0.1.6",
|
||||
"@supabase/auth-ui-svelte": "^0.2.2",
|
||||
"@supabase/supabase-js": "^2.21.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@fontsource/fira-mono": "^4.5.0",
|
||||
"@fontsource/fira-mono": "^4.5.10",
|
||||
"@neoconfetti/svelte": "^1.0.0",
|
||||
"@sveltejs/adapter-auto": "^2.0.1",
|
||||
"@sveltejs/kit": "^1.15.9",
|
||||
"@sveltejs/kit": "^1.16.3",
|
||||
"@types/cookie": "^0.5.1",
|
||||
"prettier": "^2.6.2",
|
||||
"prettier-plugin-svelte": "^2.7.0",
|
||||
"svelte": "^3.58.0",
|
||||
"svelte-check": "^3.2.0",
|
||||
"prettier": "^2.8.8",
|
||||
"prettier-plugin-svelte": "^2.10.0",
|
||||
"svelte": "^3.59.1",
|
||||
"svelte-check": "^3.3.2",
|
||||
"svelte-preprocess": "^5.0.3",
|
||||
"tslib": "^2.3.1",
|
||||
"typescript": "^4.7.4",
|
||||
"vite": "^4.0.0"
|
||||
"tslib": "^2.5.0",
|
||||
"typescript": "^5.0.4",
|
||||
"vite": "^4.3.5"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/android-arm": {
|
||||
@@ -447,47 +447,47 @@
|
||||
"integrity": "sha512-Gfkvwk9o9kE9r9XNBmJRfV8zONvXThnm1tcuojL04Uy5uRyqg93DC83lDebl0rocZCfKSjUv+fWYtMQmEDJldg=="
|
||||
},
|
||||
"node_modules/@supabase/auth-helpers-shared": {
|
||||
"version": "0.3.4",
|
||||
"resolved": "https://registry.npmjs.org/@supabase/auth-helpers-shared/-/auth-helpers-shared-0.3.4.tgz",
|
||||
"integrity": "sha512-8I3D0SPHHexkFTSfPg0nZgJSVazWoB3tZDpT6IjRv6w89vDMUzO99Lit4H9anVLF4teW8c/nJJxawDNbDlgbgg==",
|
||||
"version": "0.4.0",
|
||||
"resolved": "https://registry.npmjs.org/@supabase/auth-helpers-shared/-/auth-helpers-shared-0.4.0.tgz",
|
||||
"integrity": "sha512-5wlCGb4HZ46W2fg8tdi22ZU1GAIOe3YxXrTgvAjWVapNFN+PiH5cFIbz51ZgZRK1TokRZ01pB1GEFy4e+NZE+A==",
|
||||
"dependencies": {
|
||||
"jose": "^4.14.0"
|
||||
"jose": "^4.14.3"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@supabase/supabase-js": "^2.0.4"
|
||||
"@supabase/supabase-js": "^2.19.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@supabase/auth-helpers-sveltekit": {
|
||||
"version": "0.9.4",
|
||||
"resolved": "https://registry.npmjs.org/@supabase/auth-helpers-sveltekit/-/auth-helpers-sveltekit-0.9.4.tgz",
|
||||
"integrity": "sha512-Z/ooQZqoT4SefwpZrqYr57Ynt1k8dND0T/8/buYhkZXV5OGf8v1R5zrZI1pzyBOCF7FzOIBuo+hmMZhytZnjag==",
|
||||
"version": "0.10.0",
|
||||
"resolved": "https://registry.npmjs.org/@supabase/auth-helpers-sveltekit/-/auth-helpers-sveltekit-0.10.0.tgz",
|
||||
"integrity": "sha512-Pq0BiN1ms+8Q7yxoRufhoayvI9YoDYTkIJ8EXy3MJeeI3n2meNUF8x6h/CRsWQ79pLmulESzIiJTkSVVGn/YlQ==",
|
||||
"dependencies": {
|
||||
"@supabase/auth-helpers-shared": "0.3.4"
|
||||
"@supabase/auth-helpers-shared": "0.4.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@supabase/supabase-js": "^2.15.0",
|
||||
"@sveltejs/kit": "^1.15.2"
|
||||
"@supabase/supabase-js": "^2.19.0",
|
||||
"@sveltejs/kit": "^1.15.4"
|
||||
}
|
||||
},
|
||||
"node_modules/@supabase/auth-ui-shared": {
|
||||
"version": "0.1.3",
|
||||
"resolved": "https://registry.npmjs.org/@supabase/auth-ui-shared/-/auth-ui-shared-0.1.3.tgz",
|
||||
"integrity": "sha512-GoyW+3EiDcy+sQdmdkezx9fpsRSogHJwTdzxgf79PjyLbpyAKCnTVhOB02tiMHZ0uIZ+afAQ8CmNEwwxwHVnJw==",
|
||||
"version": "0.1.6",
|
||||
"resolved": "https://registry.npmjs.org/@supabase/auth-ui-shared/-/auth-ui-shared-0.1.6.tgz",
|
||||
"integrity": "sha512-dBlP2XR5KSSCBMgkWJMkc2UVA21V5AobKmekwIiHVvyVtzAiFqE5XWJiPV+kMlnRLzFXDeA0Z/CqdKTL/Kbs4A==",
|
||||
"peerDependencies": {
|
||||
"@supabase/supabase-js": "^2.8.0"
|
||||
"@supabase/supabase-js": "^2.21.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@supabase/auth-ui-svelte": {
|
||||
"version": "0.1.5",
|
||||
"resolved": "https://registry.npmjs.org/@supabase/auth-ui-svelte/-/auth-ui-svelte-0.1.5.tgz",
|
||||
"integrity": "sha512-LdpUP1nTwPRWy6R6NnA/iUA2K12Z06RKXKiMYbN4iAmKQBjHdXOZt070KNg7/5JYfaDtPdC9jNtE4ATrTWc5bQ==",
|
||||
"version": "0.2.2",
|
||||
"resolved": "https://registry.npmjs.org/@supabase/auth-ui-svelte/-/auth-ui-svelte-0.2.2.tgz",
|
||||
"integrity": "sha512-LDXpyus3wjjqLCuZgneVZuPtNvE6ztDAEOgsojpCGSjHLkM5RrKOPBFGTiLlN6awu055v2OFHS2EV5N6Fba0sQ==",
|
||||
"dependencies": {
|
||||
"@stitches/core": "^1.2.8",
|
||||
"@supabase/auth-ui-shared": "0.1.3",
|
||||
"@supabase/auth-ui-shared": "0.1.6",
|
||||
"svelte": "^3.55.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@supabase/supabase-js": "^2.8.0"
|
||||
"@supabase/supabase-js": "^2.21.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@supabase/functions-js": {
|
||||
@@ -558,9 +558,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@sveltejs/kit": {
|
||||
"version": "1.15.9",
|
||||
"resolved": "https://registry.npmjs.org/@sveltejs/kit/-/kit-1.15.9.tgz",
|
||||
"integrity": "sha512-Og+4WlguPVPS0PmAHefp4KxvTVZfyDN09aORVXIdKSzqzodSJiLs7Fhi/Q0z0YjmcoNLWF24tI0a6mTusL6Yfg==",
|
||||
"version": "1.16.3",
|
||||
"resolved": "https://registry.npmjs.org/@sveltejs/kit/-/kit-1.16.3.tgz",
|
||||
"integrity": "sha512-8uv0udYRpVuE1BweFidcWHfL+u2gAANKmvIal1dN/FWPBl7DJYbt9zYEtr3bNTiXystT8Sn0Wp54RfwpbPqHjQ==",
|
||||
"hasInstallScript": true,
|
||||
"dependencies": {
|
||||
"@sveltejs/vite-plugin-svelte": "^2.1.1",
|
||||
@@ -572,7 +572,7 @@
|
||||
"magic-string": "^0.30.0",
|
||||
"mime": "^3.0.0",
|
||||
"sade": "^1.8.1",
|
||||
"set-cookie-parser": "^2.5.1",
|
||||
"set-cookie-parser": "^2.6.0",
|
||||
"sirv": "^2.0.2",
|
||||
"tiny-glob": "^0.2.9",
|
||||
"undici": "~5.22.0"
|
||||
@@ -1111,9 +1111,9 @@
|
||||
"integrity": "sha512-cyA56iCMHAh5CdzjJIa4aohJyeO1YbwLi3Jc35MmRU6poroFjIGZzUzupGiRPOjgHg9TLu43xbpwXk523fMxKA=="
|
||||
},
|
||||
"node_modules/jose": {
|
||||
"version": "4.14.3",
|
||||
"resolved": "https://registry.npmjs.org/jose/-/jose-4.14.3.tgz",
|
||||
"integrity": "sha512-YPM9Q+dmsna4CGWNn5+oHFsuXJdxvKAOVoNjpe2nje3odSoX5Xz4s71rP50vM8uUKJyQtMnEGPmbVCVR+G4W5g==",
|
||||
"version": "4.14.4",
|
||||
"resolved": "https://registry.npmjs.org/jose/-/jose-4.14.4.tgz",
|
||||
"integrity": "sha512-j8GhLiKmUAh+dsFXlX1aJCbt5KMibuKb+d7j1JaOJG6s2UjX1PQlW+OKB/sD4a/5ZYF4RcmYmLSndOoU3Lt/3g==",
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/panva"
|
||||
}
|
||||
@@ -1578,17 +1578,17 @@
|
||||
}
|
||||
},
|
||||
"node_modules/svelte": {
|
||||
"version": "3.58.0",
|
||||
"resolved": "https://registry.npmjs.org/svelte/-/svelte-3.58.0.tgz",
|
||||
"integrity": "sha512-brIBNNB76mXFmU/Kerm4wFnkskBbluBDCjx/8TcpYRb298Yh2dztS2kQ6bhtjMcvUhd5ynClfwpz5h2gnzdQ1A==",
|
||||
"version": "3.59.1",
|
||||
"resolved": "https://registry.npmjs.org/svelte/-/svelte-3.59.1.tgz",
|
||||
"integrity": "sha512-pKj8fEBmqf6mq3/NfrB9SLtcJcUvjYSWyePlfCqN9gujLB25RitWK8PvFzlwim6hD/We35KbPlRteuA6rnPGcQ==",
|
||||
"engines": {
|
||||
"node": ">= 8"
|
||||
}
|
||||
},
|
||||
"node_modules/svelte-check": {
|
||||
"version": "3.2.0",
|
||||
"resolved": "https://registry.npmjs.org/svelte-check/-/svelte-check-3.2.0.tgz",
|
||||
"integrity": "sha512-6ZnscN8dHEN5Eq5LgIzjj07W9nc9myyBH+diXsUAuiY/3rt0l65/LCIQYlIuoFEjp2F1NhXqZiJwV9omPj9tMw==",
|
||||
"version": "3.3.2",
|
||||
"resolved": "https://registry.npmjs.org/svelte-check/-/svelte-check-3.3.2.tgz",
|
||||
"integrity": "sha512-67j3rI0LDc2DvL0ON/2pvCasVVD3nHDrTkZNr4eITNfo2oFXdw7SIyMOiFj4swu+pjmFQAigytBK1IWyik8dBw==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@jridgewell/trace-mapping": "^0.3.17",
|
||||
@@ -1607,19 +1607,6 @@
|
||||
"svelte": "^3.55.0"
|
||||
}
|
||||
},
|
||||
"node_modules/svelte-check/node_modules/typescript": {
|
||||
"version": "5.0.4",
|
||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.0.4.tgz",
|
||||
"integrity": "sha512-cW9T5W9xY37cc+jfEnaUvX91foxtHkza3Nw3wkoF4sSlKn0MONdkdEndig/qPBWXNkmplh3NzayQzCiHM4/hqw==",
|
||||
"dev": true,
|
||||
"bin": {
|
||||
"tsc": "bin/tsc",
|
||||
"tsserver": "bin/tsserver"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12.20"
|
||||
}
|
||||
},
|
||||
"node_modules/svelte-hmr": {
|
||||
"version": "0.15.1",
|
||||
"resolved": "https://registry.npmjs.org/svelte-hmr/-/svelte-hmr-0.15.1.tgz",
|
||||
@@ -1759,16 +1746,16 @@
|
||||
}
|
||||
},
|
||||
"node_modules/typescript": {
|
||||
"version": "4.9.5",
|
||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.5.tgz",
|
||||
"integrity": "sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==",
|
||||
"version": "5.0.4",
|
||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.0.4.tgz",
|
||||
"integrity": "sha512-cW9T5W9xY37cc+jfEnaUvX91foxtHkza3Nw3wkoF4sSlKn0MONdkdEndig/qPBWXNkmplh3NzayQzCiHM4/hqw==",
|
||||
"dev": true,
|
||||
"bin": {
|
||||
"tsc": "bin/tsc",
|
||||
"tsserver": "bin/tsserver"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=4.2.0"
|
||||
"node": ">=12.20"
|
||||
}
|
||||
},
|
||||
"node_modules/undici": {
|
||||
@@ -1795,9 +1782,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/vite": {
|
||||
"version": "4.3.3",
|
||||
"resolved": "https://registry.npmjs.org/vite/-/vite-4.3.3.tgz",
|
||||
"integrity": "sha512-MwFlLBO4udZXd+VBcezo3u8mC77YQk+ik+fbc0GZWGgzfbPP+8Kf0fldhARqvSYmtIWoAJ5BXPClUbMTlqFxrA==",
|
||||
"version": "4.3.5",
|
||||
"resolved": "https://registry.npmjs.org/vite/-/vite-4.3.5.tgz",
|
||||
"integrity": "sha512-0gEnL9wiRFxgz40o/i/eTBwm+NEbpUeTWhzKrZDSdKm6nplj+z4lKz8ANDgildxHm47Vg8EUia0aicKbawUVVA==",
|
||||
"dependencies": {
|
||||
"esbuild": "^0.17.5",
|
||||
"postcss": "^8.4.23",
|
||||
|
||||
@@ -11,25 +11,25 @@
|
||||
"format": "prettier --plugin-search-dir . --write ."
|
||||
},
|
||||
"devDependencies": {
|
||||
"@fontsource/fira-mono": "^4.5.0",
|
||||
"@fontsource/fira-mono": "^4.5.10",
|
||||
"@neoconfetti/svelte": "^1.0.0",
|
||||
"@sveltejs/adapter-auto": "^2.0.1",
|
||||
"@sveltejs/kit": "^1.15.9",
|
||||
"@sveltejs/kit": "^1.16.3",
|
||||
"@types/cookie": "^0.5.1",
|
||||
"prettier": "^2.6.2",
|
||||
"prettier-plugin-svelte": "^2.7.0",
|
||||
"svelte": "^3.58.0",
|
||||
"svelte-check": "^3.2.0",
|
||||
"prettier": "^2.8.8",
|
||||
"prettier-plugin-svelte": "^2.10.0",
|
||||
"svelte": "^3.59.1",
|
||||
"svelte-check": "^3.3.2",
|
||||
"svelte-preprocess": "^5.0.3",
|
||||
"tslib": "^2.3.1",
|
||||
"typescript": "^4.7.4",
|
||||
"vite": "^4.0.0"
|
||||
"tslib": "^2.5.0",
|
||||
"typescript": "^5.0.4",
|
||||
"vite": "^4.3.5"
|
||||
},
|
||||
"type": "module",
|
||||
"dependencies": {
|
||||
"@supabase/auth-helpers-sveltekit": "^0.9.4",
|
||||
"@supabase/auth-ui-shared": "^0.1.3",
|
||||
"@supabase/auth-ui-svelte": "^0.1.5",
|
||||
"@supabase/auth-helpers-sveltekit": "^0.10.0",
|
||||
"@supabase/auth-ui-shared": "^0.1.6",
|
||||
"@supabase/auth-ui-svelte": "^0.2.2",
|
||||
"@supabase/supabase-js": "^2.21.0"
|
||||
}
|
||||
}
|
||||
@@ -1,4 +0,0 @@
|
||||
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);
|
||||
@@ -6,6 +6,7 @@
|
||||
|
||||
export let data
|
||||
|
||||
let { supabase, session } = data
|
||||
$: ({ supabase, session } = data)
|
||||
|
||||
onMount(() => {
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
<Auth
|
||||
supabaseClient={data.supabase}
|
||||
view="magic_link"
|
||||
redirectTo={`${data.url}/logging-in?redirect=/`}
|
||||
redirectTo={`${data.url}/auth/callback`}
|
||||
showLinks={false}
|
||||
appearance={{ theme: ThemeSupa, style: { input: 'color: #fff' } }}
|
||||
/>
|
||||
|
||||
@@ -7,8 +7,9 @@
|
||||
export let form
|
||||
|
||||
let { session, supabase, profile } = data
|
||||
$: ({ session, supabase, profile } = data)
|
||||
|
||||
let profileForm: any
|
||||
let profileForm: HTMLFormElement
|
||||
let loading = false
|
||||
let fullName: string = profile?.full_name ?? ''
|
||||
let username: string = profile?.username ?? ''
|
||||
@@ -49,7 +50,7 @@
|
||||
/>
|
||||
<div>
|
||||
<label for="email">Email</label>
|
||||
<input id="email" type="text" value={session?.user.email} disabled />
|
||||
<input name="email" id="email" type="text" value={session?.user.email} disabled />
|
||||
</div>
|
||||
|
||||
<div>
|
||||
|
||||
@@ -40,15 +40,18 @@
|
||||
|
||||
const file = files[0]
|
||||
const fileExt = file.name.split('.').pop()
|
||||
url = `${Math.random()}.${fileExt}`
|
||||
const filePath = `${Math.random()}.${fileExt}`
|
||||
|
||||
let { error } = await supabase.storage.from('avatars').upload(url, file)
|
||||
let { error } = await supabase.storage.from('avatars').upload(filePath, file)
|
||||
|
||||
if (error) {
|
||||
throw error
|
||||
}
|
||||
|
||||
dispatch('upload')
|
||||
url = filePath
|
||||
setTimeout(() => {
|
||||
dispatch('upload')
|
||||
}, 100)
|
||||
} catch (error) {
|
||||
if (error instanceof Error) {
|
||||
alert(error.message)
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
import { redirect } from '@sveltejs/kit'
|
||||
|
||||
export const GET = async ({ url, locals: { supabase } }) => {
|
||||
const code = url.searchParams.get('code')
|
||||
|
||||
if (code) {
|
||||
await supabase.auth.exchangeCodeForSession(code)
|
||||
}
|
||||
|
||||
throw redirect(303, '/account')
|
||||
}
|
||||
@@ -1,22 +0,0 @@
|
||||
<!-- src/routes/logging-in/+page.svelte -->
|
||||
<script lang="ts">
|
||||
import { browser } from '$app/environment'
|
||||
import { goto } from '$app/navigation'
|
||||
import { page } from '$app/stores'
|
||||
|
||||
export let data
|
||||
$: {
|
||||
const redirectTo = $page.url.searchParams.get('redirect')
|
||||
|
||||
// check if user has been set in session store then redirect
|
||||
if (browser && data.session) {
|
||||
goto(redirectTo ?? '/account')
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<section>
|
||||
"Because as we know, there are known knowns; there are things we know we know. We also know there
|
||||
are known unknowns; that is to say we know there are some things we do not know. But there are
|
||||
also unknown unknowns—the ones we don't know we don't know" - Donald Rumsfeld
|
||||
</section>
|
||||
@@ -58,6 +58,7 @@
|
||||
"framer-motion": "^6.5.1",
|
||||
"github-slugger": "^2.0.0",
|
||||
"gray-matter": "^4.0.3",
|
||||
"hast-util-has-property": "^2.0.1",
|
||||
"isbot": "^3.6.5",
|
||||
"jsrsasign": "^10.5.26",
|
||||
"lodash": "^4.17.21",
|
||||
@@ -94,12 +95,17 @@
|
||||
"ui": "*",
|
||||
"unist-builder": "^3.0.1",
|
||||
"unist-util-filter": "^4.0.1",
|
||||
"unist-util-visit": "^4.1.2",
|
||||
"uuid": "^9.0.0",
|
||||
"valtio": "^1.7.6"
|
||||
"valtio": "^1.7.6",
|
||||
"yargs": "^17.7.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/hast": "^2.3.4",
|
||||
"@types/node": "^17.0.12",
|
||||
"@types/react": "17.0.39",
|
||||
"@types/unist": "^2.0.6",
|
||||
"@types/yargs": "^17.0.24",
|
||||
"config": "*",
|
||||
"dotenv": "^16.0.3",
|
||||
"ejs": "^3.1.8",
|
||||
@@ -257,6 +263,15 @@
|
||||
"csstype": "^3.0.2"
|
||||
}
|
||||
},
|
||||
"apps/docs/node_modules/@types/yargs": {
|
||||
"version": "17.0.24",
|
||||
"resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.24.tgz",
|
||||
"integrity": "sha512-6i0aC7jV6QzQB8ne1joVZ0eSFIstHsCrobmOtghM11yGlH0j43FKL2UhWdELkyps0zuf7qVTUVCCR+tgSlyLLw==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@types/yargs-parser": "*"
|
||||
}
|
||||
},
|
||||
"apps/docs/node_modules/ajv": {
|
||||
"version": "6.12.6",
|
||||
"dev": true,
|
||||
@@ -297,6 +312,19 @@
|
||||
"url": "https://github.com/chalk/chalk?sponsor=1"
|
||||
}
|
||||
},
|
||||
"apps/docs/node_modules/cliui": {
|
||||
"version": "8.0.1",
|
||||
"resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz",
|
||||
"integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==",
|
||||
"dependencies": {
|
||||
"string-width": "^4.2.0",
|
||||
"strip-ansi": "^6.0.1",
|
||||
"wrap-ansi": "^7.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"apps/docs/node_modules/clsx": {
|
||||
"version": "1.2.1",
|
||||
"license": "MIT",
|
||||
@@ -664,6 +692,31 @@
|
||||
"version": "6.0.0",
|
||||
"license": "MIT"
|
||||
},
|
||||
"apps/docs/node_modules/yargs": {
|
||||
"version": "17.7.2",
|
||||
"resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz",
|
||||
"integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==",
|
||||
"dependencies": {
|
||||
"cliui": "^8.0.1",
|
||||
"escalade": "^3.1.1",
|
||||
"get-caller-file": "^2.0.5",
|
||||
"require-directory": "^2.1.1",
|
||||
"string-width": "^4.2.3",
|
||||
"y18n": "^5.0.5",
|
||||
"yargs-parser": "^21.1.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"apps/docs/node_modules/yargs-parser": {
|
||||
"version": "21.1.1",
|
||||
"resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz",
|
||||
"integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"apps/www": {
|
||||
"version": "0.0.3",
|
||||
"license": "MIT",
|
||||
@@ -24602,7 +24655,6 @@
|
||||
"version": "2.0.5",
|
||||
"resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz",
|
||||
"integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": "6.* || 8.* || >= 10.*"
|
||||
}
|
||||
@@ -39584,7 +39636,6 @@
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz",
|
||||
"integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
@@ -45404,7 +45455,6 @@
|
||||
"version": "7.0.0",
|
||||
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz",
|
||||
"integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"ansi-styles": "^4.0.0",
|
||||
"string-width": "^4.1.0",
|
||||
@@ -45517,7 +45567,6 @@
|
||||
"version": "5.0.8",
|
||||
"resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz",
|
||||
"integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
@@ -62917,8 +62966,11 @@
|
||||
"@supabase/auth-helpers-nextjs": "^0.5.6",
|
||||
"@supabase/auth-helpers-react": "^0.3.1",
|
||||
"@supabase/supabase-js": "^2.13.0",
|
||||
"@types/hast": "^2.3.4",
|
||||
"@types/node": "^17.0.12",
|
||||
"@types/react": "17.0.39",
|
||||
"@types/unist": "^2.0.6",
|
||||
"@types/yargs": "^17.0.24",
|
||||
"algoliasearch": "^4.14.2",
|
||||
"babel": "^6.23.0",
|
||||
"clsx": "^1.2.1",
|
||||
@@ -62931,6 +62983,7 @@
|
||||
"github-slugger": "^2.0.0",
|
||||
"globby": "^12.0.2",
|
||||
"gray-matter": "^4.0.3",
|
||||
"hast-util-has-property": "^2.0.1",
|
||||
"isbot": "^3.6.5",
|
||||
"jsrsasign": "^10.5.26",
|
||||
"lodash": "^4.17.21",
|
||||
@@ -62975,8 +63028,10 @@
|
||||
"ui": "*",
|
||||
"unist-builder": "^3.0.1",
|
||||
"unist-util-filter": "^4.0.1",
|
||||
"unist-util-visit": "^4.1.2",
|
||||
"uuid": "^9.0.0",
|
||||
"valtio": "^1.7.6"
|
||||
"valtio": "^1.7.6",
|
||||
"yargs": "^17.7.2"
|
||||
},
|
||||
"dependencies": {
|
||||
"@babel/core": {
|
||||
@@ -63079,6 +63134,15 @@
|
||||
"csstype": "^3.0.2"
|
||||
}
|
||||
},
|
||||
"@types/yargs": {
|
||||
"version": "17.0.24",
|
||||
"resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.24.tgz",
|
||||
"integrity": "sha512-6i0aC7jV6QzQB8ne1joVZ0eSFIstHsCrobmOtghM11yGlH0j43FKL2UhWdELkyps0zuf7qVTUVCCR+tgSlyLLw==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@types/yargs-parser": "*"
|
||||
}
|
||||
},
|
||||
"ajv": {
|
||||
"version": "6.12.6",
|
||||
"dev": true,
|
||||
@@ -63104,6 +63168,16 @@
|
||||
"supports-color": "^7.1.0"
|
||||
}
|
||||
},
|
||||
"cliui": {
|
||||
"version": "8.0.1",
|
||||
"resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz",
|
||||
"integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==",
|
||||
"requires": {
|
||||
"string-width": "^4.2.0",
|
||||
"strip-ansi": "^6.0.1",
|
||||
"wrap-ansi": "^7.0.0"
|
||||
}
|
||||
},
|
||||
"clsx": {
|
||||
"version": "1.2.1"
|
||||
},
|
||||
@@ -63334,6 +63408,25 @@
|
||||
},
|
||||
"vscode-textmate": {
|
||||
"version": "6.0.0"
|
||||
},
|
||||
"yargs": {
|
||||
"version": "17.7.2",
|
||||
"resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz",
|
||||
"integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==",
|
||||
"requires": {
|
||||
"cliui": "^8.0.1",
|
||||
"escalade": "^3.1.1",
|
||||
"get-caller-file": "^2.0.5",
|
||||
"require-directory": "^2.1.1",
|
||||
"string-width": "^4.2.3",
|
||||
"y18n": "^5.0.5",
|
||||
"yargs-parser": "^21.1.1"
|
||||
}
|
||||
},
|
||||
"yargs-parser": {
|
||||
"version": "21.1.1",
|
||||
"resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz",
|
||||
"integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw=="
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -65672,8 +65765,7 @@
|
||||
"get-caller-file": {
|
||||
"version": "2.0.5",
|
||||
"resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz",
|
||||
"integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==",
|
||||
"dev": true
|
||||
"integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg=="
|
||||
},
|
||||
"get-intrinsic": {
|
||||
"version": "1.2.0",
|
||||
@@ -76968,8 +77060,7 @@
|
||||
"require-directory": {
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz",
|
||||
"integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==",
|
||||
"dev": true
|
||||
"integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q=="
|
||||
},
|
||||
"require-from-string": {
|
||||
"version": "2.0.2",
|
||||
@@ -82569,7 +82660,6 @@
|
||||
"version": "7.0.0",
|
||||
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz",
|
||||
"integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"ansi-styles": "^4.0.0",
|
||||
"string-width": "^4.1.0",
|
||||
@@ -82880,8 +82970,7 @@
|
||||
"y18n": {
|
||||
"version": "5.0.8",
|
||||
"resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz",
|
||||
"integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==",
|
||||
"dev": true
|
||||
"integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA=="
|
||||
},
|
||||
"yaeti": {
|
||||
"version": "0.0.6",
|
||||
|
||||
@@ -44,7 +44,7 @@ functions:
|
||||
isSpotlight: true
|
||||
code: |
|
||||
```js
|
||||
let city: CityModel = try await client.database
|
||||
let cities: [CityModel] = try await client.database
|
||||
.from("cities")
|
||||
.execute().value
|
||||
```
|
||||
@@ -53,7 +53,7 @@ functions:
|
||||
description: You can select specific fields from your tables.
|
||||
code: |
|
||||
```js
|
||||
let city: CityModel = try await client.database
|
||||
let cities: [CityModel] = try await client.database
|
||||
.from("cities")
|
||||
.select(columns:"name")
|
||||
.execute().value
|
||||
@@ -63,7 +63,7 @@ functions:
|
||||
description: If your database has foreign key relationships, you can query related tables too.
|
||||
code: |
|
||||
```js
|
||||
let country: CountryModel = try await client.database
|
||||
let countries: [CountryModel] = try await client.database
|
||||
.from("countries")
|
||||
.select(columns:"""
|
||||
id,
|
||||
@@ -92,7 +92,7 @@ functions:
|
||||
(both in the users) table
|
||||
code: |
|
||||
```js
|
||||
let message: MessageModel = try await client.database
|
||||
let messages: [MessageModel] = try await client.database
|
||||
.from("messages")
|
||||
.select(columns:"""
|
||||
content,
|
||||
@@ -123,7 +123,7 @@ functions:
|
||||
[PostgREST docs](http://postgrest.org/en/v7.0.0/api.html#json-columns) for more details.
|
||||
code: |
|
||||
```js
|
||||
let country: UserModel = try await client.database
|
||||
let users: [UserModel] = try await client.database
|
||||
.from("users")
|
||||
.select(columns:"""
|
||||
id, name
|
||||
|
||||
@@ -74,7 +74,8 @@ const PaymentTotal: FC<Props> = ({
|
||||
<div className="space-y-2">
|
||||
{amountDueImmediately < 0 ? (
|
||||
<p className="text-sm text-scale-1100">
|
||||
A total of ${Math.abs(amountDueImmediately).toFixed(2)} will be returned on{' '}
|
||||
A total of ${Math.abs(amountDueImmediately).toFixed(2)} will be returned as
|
||||
credits on{' '}
|
||||
<span className="text-brand-900">
|
||||
{billingDate.toLocaleDateString('en-US', {
|
||||
day: 'numeric',
|
||||
|
||||
@@ -60,13 +60,7 @@ const EdgeFunctionDetails: FC<Props> = () => {
|
||||
: '[YOUR ANON KEY]'
|
||||
|
||||
const endpoint = apiService?.app_config.endpoint ?? ''
|
||||
const endpointSections = endpoint.split('.')
|
||||
const functionsEndpoint = [
|
||||
...endpointSections.slice(0, 1),
|
||||
'functions',
|
||||
...endpointSections.slice(1),
|
||||
].join('.')
|
||||
const functionUrl = `${apiService?.protocol}://${functionsEndpoint}/${selectedFunction?.slug}`
|
||||
const functionUrl = `${apiService?.protocol}://${endpoint}/functions/v1/${selectedFunction?.slug}`
|
||||
|
||||
const { managementCommands, secretCommands, invokeCommands } = generateCLICommands(
|
||||
selectedFunction,
|
||||
|
||||
@@ -23,7 +23,7 @@ const EdgeFunctionsListItem: FC<Props> = ({ function: item }) => {
|
||||
// get the .co or .net TLD from the restUrl
|
||||
const restUrl = ui.selectedProject?.restUrl
|
||||
const restUrlTld = new URL(restUrl as string).hostname.split('.').pop()
|
||||
const functionUrl = `https://${ref}.functions.supabase.${restUrlTld}/${item.slug}`
|
||||
const functionUrl = `https://${ref}.supabase.${restUrlTld}/functions/v1/${item.slug}`
|
||||
|
||||
return (
|
||||
<Table.tr
|
||||
|
||||
@@ -26,12 +26,7 @@ const TerminalInstructions: FC<Props> = ({ closable = false, removeBorder = fals
|
||||
: '[YOUR ANON KEY]'
|
||||
const endpoint = settings?.autoApiService.app_config.endpoint ?? ''
|
||||
|
||||
const endpointSections = endpoint.split('.')
|
||||
const functionsEndpoint = [
|
||||
...endpointSections.slice(0, 1),
|
||||
'functions',
|
||||
...endpointSections.slice(1),
|
||||
].join('.')
|
||||
const functionsEndpoint = `${endpoint}/functions/v1`
|
||||
|
||||
// get the .co or .net TLD from the restUrl
|
||||
const restUrl = settings?.autoApiService.restUrl
|
||||
@@ -64,7 +59,7 @@ const TerminalInstructions: FC<Props> = ({ closable = false, removeBorder = fals
|
||||
comment: 'Deploy your function',
|
||||
},
|
||||
{
|
||||
command: `curl -L -X POST 'https://${projectRef}.functions.supabase.${restUrlTld}/hello-world' -H 'Authorization: Bearer ${
|
||||
command: `curl -L -X POST 'https://${projectRef}.supabase.${restUrlTld}/functions/v1/hello-world' -H 'Authorization: Bearer ${
|
||||
anonKey ?? '[YOUR ANON KEY]'
|
||||
}' --data '{"name":"Functions"}'`,
|
||||
description: 'Invokes the hello-world function',
|
||||
|
||||