No files matched your search
@@ -96,4 +96,6 @@ On the Next.JS side of things, these work almost exactly the same as the client
|
||||
|
||||
#### Search
|
||||
|
||||
Search is handled through Algolia. When the site is built, a [search script](https://github.com/supabase/supabase/blob/master/apps/docs/scripts/build-search.ts) runs through all of the types of content, generating search objects that are sent to Algolia to index.
|
||||
Search is handled using a Supabase instance. During CI, [a script](https://github.com/supabase/supabase/blob/master/apps/docs/scripts/search/generate-embeddings.ts) aggregates all content sources (eg. guides, reference docs, etc), indexes them using OpenAI embeddings, and stores them in a Supabase database.
|
||||
|
||||
At runtime, an [Edge Function](https://github.com/supabase/supabase/blob/master/supabase/functions) is executed that performs a similarity search between the user's query and the above content sources using [`pgvector`](https://github.com/pgvector/pgvector) embeddings.
|
||||
@@ -45,7 +45,7 @@ export const gettingstarted = {
|
||||
items: [
|
||||
{ name: 'Features', url: '/guides/getting-started/features' },
|
||||
{ name: 'Architecture', url: '/guides/getting-started/architecture' },
|
||||
// { name: 'Local Development', url: '/guides/cli/local-development' }, // Disabling this until we actually move the doc into this section
|
||||
{ name: 'Local Development', url: '/guides/getting-started/local-development' },
|
||||
{
|
||||
name: 'Framework Quickstarts',
|
||||
items: [
|
||||
@@ -141,7 +141,6 @@ export const cli = {
|
||||
title: 'CLI',
|
||||
items: [
|
||||
{ name: 'Overview', url: '/guides/cli' },
|
||||
{ name: 'Local Development', url: '/guides/cli/local-development' },
|
||||
{ name: 'Managing Environments', url: '/guides/cli/managing-environments' },
|
||||
],
|
||||
}
|
||||
@@ -688,7 +687,6 @@ export const supabase_cli = {
|
||||
url: '/guides/cli',
|
||||
items: [
|
||||
{ name: 'Overview', url: '/guides/cli' },
|
||||
{ name: 'Local Development', url: '/guides/cli/local-development' },
|
||||
{ name: 'Managing Environments', url: '/guides/cli/managing-environments' },
|
||||
{
|
||||
name: 'Reference',
|
||||
|
||||
@@ -8,6 +8,7 @@ import { memo, useEffect } from 'react'
|
||||
import Footer from '~/components/Navigation/Footer'
|
||||
import { menuState, useMenuLevelId, useMenuMobileOpen } from '~/hooks/useMenuState'
|
||||
import Head from 'next/head'
|
||||
import { Announcement, AnnouncementCountdown } from 'ui'
|
||||
|
||||
const levelsData = {
|
||||
home: {
|
||||
@@ -324,6 +325,11 @@ const SiteLayout = ({ children }) => {
|
||||
<title>Supabase Docs</title>
|
||||
</Head>
|
||||
<main>
|
||||
<div>
|
||||
<Announcement>
|
||||
<AnnouncementCountdown />
|
||||
</Announcement>
|
||||
</div>
|
||||
<div className="flex flex-row h-screen">
|
||||
<NavContainer />
|
||||
<Container>
|
||||
|
||||
@@ -9,9 +9,9 @@
|
||||
"start": "next start",
|
||||
"lint": "next lint",
|
||||
"build:sitemap": "node ./internals/generate-sitemap.mjs",
|
||||
"embeddings": "tsx scripts/generate-embeddings.ts",
|
||||
"embeddings": "tsx scripts/search/generate-embeddings.ts",
|
||||
"embeddings:refresh": "npm run embeddings -- --refresh",
|
||||
"postbuild": "ts-node ./scripts/build-search.ts && node ./internals/generate-sitemap.mjs",
|
||||
"postbuild": "node ./internals/generate-sitemap.mjs",
|
||||
"generate:all": "npm-run-all --parallel gen:api gen:cli gen:gotrue gen:storage gen:supabase-dart:v0 gen:supabase-dart:v1 gen:supabase-csharp:v0 gen:supabase-js:v1 gen:supabase-js:v2 gen:realtime",
|
||||
"gen:api": "npm-run-all gen:api:usage",
|
||||
"gen:api:usage": "ts-node ./generator/index.ts gen --type api --url https://api.supabase.com --input ../../spec/transforms/api_v0_openapi_deparsed.json --output ./docs/reference/api/generated/usage.mdx",
|
||||
|
||||
@@ -127,7 +127,7 @@ GET https://[REF].supabase.co/rest/v1/countries?select=id,name,cities(id,name)
|
||||
|
||||
## Many-to-many joins
|
||||
|
||||
The Serverless APIs will detect many-to-many joins. For example, if you have a database which stored teams of users (where each user could below to many teams):
|
||||
The Serverless APIs will detect many-to-many joins. For example, if you have a database which stored teams of users (where each user could belong to many teams):
|
||||
|
||||
```sql
|
||||
create table
|
||||
|
||||
@@ -118,7 +118,7 @@ brew upgrade supabase
|
||||
|
||||
- [Supabase CLI Reference](/docs/reference/cli/introduction)
|
||||
- [Supabase CLI Configuration](/docs/reference/cli/config)
|
||||
- [Local Development](/docs/guides/cli/local-development)
|
||||
- [Local Development](/docs/guides/getting-started/local-development)
|
||||
- [Managing Environments](/docs/guides/cli/managing-environments)
|
||||
|
||||
export const Page = ({ children }) => <Layout meta={meta} children={children} />
|
||||
|
||||
@@ -133,7 +133,7 @@ edge build supabase_functions
|
||||
supabase functions deploy dart_edge
|
||||
```
|
||||
|
||||
You will be asked to provide the project reference of your Supabase instance to which you want to deploy the function. You can [link your local project to your Supabase instance](https://supabase.com/docs/guides/cli/local-development#link-your-project) to avoid having to provide the Supabase reference every time you deploy.
|
||||
You will be asked to provide the project reference of your Supabase instance to which you want to deploy the function. You can [link your local project to your Supabase instance](https://supabase.com/docs/guides/getting-started/local-development#link-your-project) to avoid having to provide the Supabase reference every time you deploy.
|
||||
|
||||
export const Page = ({ children }) => <Layout meta={meta} children={children} />
|
||||
|
||||
|
||||
@@ -25,7 +25,7 @@ Follow the steps to prepare your Supabase project on your local machine.
|
||||
|
||||
- Install the Supabase CLI. [Docs](/docs/guides/cli).
|
||||
- Login to the CLI using the command: `supabase login`. [Docs](/docs/reference/cli/usage#supabase-login).
|
||||
- Initialize Supabase inside your project using the command: `supabase init`. [Docs](/docs/guides/cli/local-development#getting-started).
|
||||
- Initialize Supabase inside your project using the command: `supabase init`. [Docs](/docs/guides/getting-started/local-development#getting-started).
|
||||
- Link to your Remote Project using the command `supabase link --project-ref your-project-ref`. [Docs](/docs/reference/cli/usage#supabase-link).
|
||||
- Optional: Setup your environment: Follow [this setup guide](https://deno.land/manual/getting_started/setup_your_environment) to integrate the Deno language server with your editor.
|
||||
|
||||
|
||||
@@ -286,7 +286,7 @@ export const resources = [
|
||||
{
|
||||
title: 'Local Development',
|
||||
hasLightIcon: true,
|
||||
href: '/guides/cli/local-development',
|
||||
href: '/guides/getting-started/local-development',
|
||||
description: 'Use the Supabase CLI to develop locally and collaborate between teams.',
|
||||
},
|
||||
]
|
||||
|
||||
@@ -44,7 +44,7 @@ Encrypt sensitive data and store secrets using our Postgres extension, Supabase
|
||||
|
||||
### Database migrations
|
||||
|
||||
Develop locally and push your changes to your production database using migrations. [Docs](/docs/guides/cli/local-development#database-migrations)
|
||||
Develop locally and push your changes to your production database using migrations. [Docs](/docs/guides/getting-started/local-development#database-migrations)
|
||||
|
||||
<br />
|
||||
|
||||
|
||||
@@ -359,6 +359,10 @@ create table "public"."cities" (
|
||||
|
||||
```
|
||||
|
||||
Alternately, you can view your table definitions directly from the Table Editor:
|
||||
|
||||

|
||||
|
||||
You can then copy this SQL into a new migration file, and run `supabase db reset` to apply the changes.
|
||||
|
||||
The last step is deploying these changes to a live Supabase project.
|
||||
@@ -407,6 +411,19 @@ If your project uses Edge Functions, you can deploy these using [`functions depl
|
||||
supabase functions deploy <function_name>
|
||||
```
|
||||
|
||||
### Use Auth locally
|
||||
|
||||
To use Auth locally, update your project's `supabase/config.toml` file that gets created after running `supabase init`. Add any providers you want, and set enabled to `true`
|
||||
|
||||
```bash config.toml
|
||||
[auth.external.github]
|
||||
enabled = true
|
||||
client_id = ""
|
||||
secret = ""
|
||||
```
|
||||
|
||||
For these changes to take effect, you need to run `supabase stop` and `supabase start` again.
|
||||
|
||||
### Enabling Local Logging
|
||||
|
||||
Local logs rely on the Supabase Analytics Server. This can be enabled via the CLI configuration, and requires a Google Cloud project and BigQuery access.
|
||||
@@ -424,9 +441,9 @@ Requirements:
|
||||
|
||||
Once you have these 3 items, follow these steps:
|
||||
|
||||
1. Update your project's `config.toml` and fill in the following:
|
||||
First, update your project's `supabase/config.toml`:
|
||||
|
||||
```
|
||||
```bash
|
||||
[analytics]
|
||||
enabled = true
|
||||
gcp_project_number = "123456"
|
||||
@@ -434,9 +451,9 @@ gcp_project_id = "my-project-id"
|
||||
gcp_jwt_path = "supabase/gcloud.json"
|
||||
```
|
||||
|
||||
2. Place your service account key (a JSON file) at the corresponding path and ensure that it is correctly named.
|
||||
Next, place your service account key (a JSON file) at the corresponding path and ensure that it is correctly named.
|
||||
|
||||
3. Start your local stack using `supabase start`
|
||||
Last, start your local stack using `supabase start`
|
||||
|
||||
This will switch the logging drivers and will direct logs to the Analytics server. You will be able to view and query your logs via the Studio Logs Explorer and Logs UI.
|
||||
|
||||
@@ -68,7 +68,7 @@ export const meta = {
|
||||
<StepHikeCompact.Code>
|
||||
|
||||
```yaml pubspec.yaml
|
||||
supabase_flutter: ^1.2.2
|
||||
supabase_flutter: ^1.6.2
|
||||
```
|
||||
|
||||
</StepHikeCompact.Code>
|
||||
|
||||
@@ -126,6 +126,7 @@ export default MyApp
|
||||
Wrap your `pages/_app.tsx` component with the `SessionContextProvider` component:
|
||||
|
||||
```jsx lines=2,8 title=pages/_app.tsx
|
||||
import '../styles/globals.css';
|
||||
import { useState } from 'react'
|
||||
import { createBrowserSupabaseClient } from '@supabase/auth-helpers-nextjs'
|
||||
import { SessionContextProvider, Session } from '@supabase/auth-helpers-react'
|
||||
|
||||
@@ -38,7 +38,7 @@ Let's dive into how we actually set up and link these two platforms to create a
|
||||
|
||||
## Create a Supabase Project
|
||||
|
||||
As mentioned, while you can [deploy Supabase locally](/docs/guides/cli/local-development). For the purpose of this guide, we'll use Supabase Cloud:
|
||||
As mentioned, while you can [deploy Supabase locally](/docs/guides/getting-started/local-development). For the purpose of this guide, we'll use Supabase Cloud:
|
||||
|
||||
1. Create a **Supabase** account by signing in with GitHub.
|
||||
2. Give your organization a name (this can be changed later).
|
||||
|
||||
@@ -24,6 +24,10 @@ The video below illustrates how to get connected. Or you can read the steps belo
|
||||
></iframe>
|
||||
</div>
|
||||
|
||||
<Admonition type="note">
|
||||
PolyScale provides caching for TCP connections and GraphQL. Support for caching with the Supabase client is coming soon.
|
||||
</Admonition>
|
||||
|
||||
## Step 0: Create a PolyScale account
|
||||
|
||||
If you do not already have a PolyScale account, you can create an account [here](https://app.polyscale.ai/signup). PolyScale offers a free tier and no credit card is required.
|
||||
|
||||
@@ -23,6 +23,10 @@ Daily Backups and PITR are mutually exclusive. If your project opts into using P
|
||||
|
||||
All Pro and Enterprise tier Supabase projects are backed up automatically on a daily basis. In terms of Recovery Point Objective (RPO), Daily Backups would be suitable for projects willing to lose up to 24 hours worth of data if disaster hits at the most inopportune time. If a lower RPO is required, enabling Point-in-Time Recovery should be considered.
|
||||
|
||||
<Admonition type="note">
|
||||
For security purposes, passwords for custom roles are not stored in daily backups, and will not be found in downloadable files. As such, if you are restoring from a daily backup and are using custom roles, you will need to set their passwords once more following a completed restoration.
|
||||
</Admonition>
|
||||
|
||||
### Backup Process [#daily-backups-process]
|
||||
|
||||
The PostgreSQL utility [pg_dumpall](https://www.postgresql.org/docs/current/app-pg-dumpall.html) is used to perform daily backups. An SQL file is generated, zipped up, and sent to our storage servers for safe keeping.
|
||||
|
||||
@@ -29,7 +29,7 @@ This example assumes your Supabase project is `foobarbaz` with a corresponding A
|
||||
To get started:
|
||||
|
||||
1. [Install](/docs/guides/resources/supabase-cli) the Supabase CLI 1.11.3+.
|
||||
1. [Log in](/docs/guides/resources/supabase-cli/local-development#log-in-to-the-supabase-cli) to your Supabase account using the CLI.
|
||||
1. [Log in](/docs/guides/resources/supabase-getting-started/local-development#log-in-to-the-supabase-cli) to your Supabase account using the CLI.
|
||||
|
||||
### Configure a CNAME
|
||||
|
||||
@@ -98,7 +98,7 @@ Vanity Subdomains allow you to present a basic branded experience, compared to c
|
||||
To get started:
|
||||
|
||||
1. [Install](/docs/guides/resources/supabase-cli) the Supabase CLI 1.22.0+.
|
||||
1. [Log in](/docs/guides/resources/supabase-cli/local-development#log-in-to-the-supabase-cli) to your Supabase account using the CLI.
|
||||
1. [Log in](/docs/guides/resources/supabase-getting-started/local-development#log-in-to-the-supabase-cli) to your Supabase account using the CLI.
|
||||
1. Ensure that you have [Owner or Admin permissions](/docs/guides/platform/access-control#manage-team-members) for the project you'd like to set up a vanity subdomain for.
|
||||
1. Ensure that your project has a paid subscription (Pro/Pay as you go/Enterprise tier) in the [Billing page of the Dashboard](https://app.supabase.com/project/_/settings/billing/subscription).
|
||||
|
||||
|
||||
@@ -79,7 +79,9 @@ const NEW_PROJECT_SERVICE_KEY = 'new-project-service-key-yyy'
|
||||
|
||||
;(async () => {
|
||||
const oldSupabaseRestClient = createClient(OLD_PROJECT_URL, OLD_PROJECT_SERVICE_KEY, {
|
||||
schema: 'storage',
|
||||
db: {
|
||||
schema: 'storage'
|
||||
},
|
||||
})
|
||||
const oldSupabaseClient = createClient(OLD_PROJECT_URL, OLD_PROJECT_SERVICE_KEY)
|
||||
const newSupabaseClient = createClient(NEW_PROJECT_URL, NEW_PROJECT_SERVICE_KEY)
|
||||
|
||||
@@ -17,7 +17,7 @@ Each Supabase project comes with configurable restrictions on the IP ranges that
|
||||
To get started:
|
||||
|
||||
1. [Install](/docs/guides/cli) the Supabase CLI 1.22.0+.
|
||||
1. [Log in](/docs/guides/cli/local-development#log-in-to-the-supabase-cli) to your Supabase account using the CLI.
|
||||
1. [Log in](/docs/guides/getting-started/local-development#log-in-to-the-supabase-cli) to your Supabase account using the CLI.
|
||||
1. If your project was created before 23rd December 2022, it will need to be [upgraded to the latest Supabase version](/docs/guides/platform/migrating-and-upgrading-projects) before Network Restrictions can be used.
|
||||
1. Ensure that you have [Owner or Admin permissions](/docs/guides/platform/access-control#manage-team-members) for the project that you are enabling network restrictions.
|
||||
|
||||
|
||||
@@ -18,7 +18,7 @@ SSL enforcement only applies to connections to both Postgres and PgBouncer ("Con
|
||||
To get started:
|
||||
|
||||
1. [Install](/docs/guides/cli) the Supabase CLI 1.37.0+.
|
||||
1. [Log in](/docs/guides/cli/local-development#log-in-to-the-supabase-cli) to your Supabase account using the CLI.
|
||||
1. [Log in](/docs/guides/getting-started/local-development#log-in-to-the-supabase-cli) to your Supabase account using the CLI.
|
||||
1. Ensure that you have [Owner or Admin permissions](/docs/guides/platform/access-control#manage-team-members) for the project that you are enabling SSL enforcement.
|
||||
|
||||
## Check enforcement status
|
||||
|
||||
@@ -10,6 +10,7 @@ export const meta = {
|
||||
Supabase allows you to subscribe to real-time changes on your database from your client application.
|
||||
|
||||
You can listen to database changes using the [Postgres Changes](/docs/guides/realtime/postgres-changes) extension.
|
||||
The following video shows how you can enable this feature for your tables.
|
||||
|
||||
## Demo
|
||||
|
||||
@@ -22,6 +23,28 @@ You can listen to database changes using the [Postgres Changes](/docs/guides/rea
|
||||
></iframe>
|
||||
</div>
|
||||
|
||||
## Setup
|
||||
|
||||
You'll first need to create a `supabase_realtime` publication and add your tables (that you want to subscribe to) to the publication:
|
||||
|
||||
```sql
|
||||
begin;
|
||||
|
||||
-- remove the supabase_realtime publication
|
||||
drop
|
||||
publication if exists supabase_realtime;
|
||||
|
||||
-- re-create the supabase_realtime publication with no tables
|
||||
create publication supabase_realtime;
|
||||
|
||||
commit;
|
||||
|
||||
-- add a table called 'messages' to the publication
|
||||
-- (update this to match your tables)
|
||||
alter
|
||||
publication supabase_realtime add table messages;
|
||||
```
|
||||
|
||||
## Streaming inserts
|
||||
|
||||
You can use the `INSERT` event to stream all new rows.
|
||||
|
||||
|
After Width: | Height: | Size: 119 KiB |
@@ -235,7 +235,7 @@
|
||||
</url>
|
||||
|
||||
<url>
|
||||
<loc>https://supabase.com/docs/guides/cli/local-development</loc>
|
||||
<loc>https://supabase.com/docs/guides/getting-started/local-development</loc>
|
||||
<changefreq>weekly</changefreq>
|
||||
<changefreq>0.5</changefreq>
|
||||
</url>
|
||||
|
||||
@@ -1,155 +0,0 @@
|
||||
import fs from 'fs'
|
||||
import crypto from 'crypto'
|
||||
import path from 'path'
|
||||
import matter from 'gray-matter'
|
||||
import dotenv from 'dotenv'
|
||||
import algoliasearch from 'algoliasearch/lite'
|
||||
import { isEmpty } from 'lodash'
|
||||
|
||||
// search objects
|
||||
import { generateClientLibSearchObjects } from './files/client-libs'
|
||||
import { generateAPISearchObjects } from './files/api'
|
||||
import { generateCLISearchObjects } from './files/cli'
|
||||
|
||||
const cliObjects = generateCLISearchObjects()
|
||||
const apiObjects = generateAPISearchObjects()
|
||||
const clientLibSearchObjects = generateClientLibSearchObjects()
|
||||
|
||||
// @ts-ignore
|
||||
// The properties of the searchObject that are specifically read
|
||||
// by DocSearch are "type" and "hierarchy". The rest, though saved into Algolia (which we
|
||||
// can potentially use to craft more nuanced search experiences) are not used by DocSearch
|
||||
const ignoredFiles = [
|
||||
'pages/404.mdx',
|
||||
'pages/faq.mdx',
|
||||
'pages/support.mdx',
|
||||
'pages/oss.tsx',
|
||||
'pages/_app.tsx',
|
||||
'pages/_document.tsx',
|
||||
'pages/[...slug].tsx',
|
||||
'pages/handbook/contributing.mdx',
|
||||
'pages/handbook/introduction.mdx',
|
||||
'pages/handbook/supasquad.mdx',
|
||||
]
|
||||
|
||||
async function walk(dir) {
|
||||
let files = await fs.promises.readdir(dir)
|
||||
//@ts-ignore
|
||||
files = await Promise.all(
|
||||
files.map(async (file) => {
|
||||
const filePath = path.join(dir, file)
|
||||
const stats = await fs.promises.stat(filePath)
|
||||
if (stats.isDirectory()) return walk(filePath)
|
||||
else if (stats.isFile()) return filePath
|
||||
})
|
||||
)
|
||||
|
||||
return files.reduce((all, folderContents) => all.concat(folderContents), [])
|
||||
}
|
||||
|
||||
;(async function () {
|
||||
// initialize environment variables
|
||||
dotenv.config()
|
||||
|
||||
if (!process.env.NEXT_PUBLIC_ALGOLIA_APP_ID || !process.env.ALGOLIA_SEARCH_ADMIN_KEY) {
|
||||
return console.log(
|
||||
'Missing Algolia app ID / admin Key: skipping saving of Algolia search index'
|
||||
)
|
||||
}
|
||||
|
||||
console.log('Preparing docs indexing for Algolia')
|
||||
|
||||
try {
|
||||
const indexName = process.env.NEXT_PUBLIC_ALGOLIA_INDEX_NAME
|
||||
const client = algoliasearch(
|
||||
process.env.NEXT_PUBLIC_ALGOLIA_APP_ID,
|
||||
process.env.ALGOLIA_SEARCH_ADMIN_KEY
|
||||
)
|
||||
const index = client.initIndex(indexName)
|
||||
|
||||
const guidePages = (await walk('pages')).filter((slug) => !ignoredFiles.includes(slug))
|
||||
|
||||
// generate search objects for mdx guide pages
|
||||
const guidePagesearchObjects = guidePages
|
||||
.map((slug) => {
|
||||
let id, title, description
|
||||
const fileContents = fs.readFileSync(slug, 'utf8')
|
||||
const { data, content } = matter(fileContents)
|
||||
|
||||
if (isEmpty(data)) {
|
||||
// Guide pages do not have front-matter meta, unlike reference pages, have to manually extract
|
||||
const metaIndex = fileContents.indexOf('export const meta = {')
|
||||
if (metaIndex !== -1) {
|
||||
const metaString =
|
||||
fileContents
|
||||
.slice(metaIndex + 20, fileContents.indexOf('}', metaIndex + 1) + 1)
|
||||
.replace(/\n/g, '')
|
||||
.slice(0, -2) + '}'
|
||||
const meta = eval(`(${metaString})`)
|
||||
id = meta.id
|
||||
title = meta.title
|
||||
description = meta.description
|
||||
}
|
||||
} else {
|
||||
id = data.id
|
||||
title = data.title
|
||||
description = data.description
|
||||
}
|
||||
|
||||
const url = (slug.includes('/generated') ? slug.replace('/generated', '') : slug)
|
||||
.replace('docs', '')
|
||||
.replace('pages', '')
|
||||
.replace(/\.mdx$/, '')
|
||||
const source = slug.includes('/reference') ? 'reference' : 'guide'
|
||||
|
||||
const object = {
|
||||
// For Algolia
|
||||
objectID: crypto.randomUUID(),
|
||||
id,
|
||||
title,
|
||||
description,
|
||||
url,
|
||||
source,
|
||||
//pageContent: content,
|
||||
pageContent: '',
|
||||
category: undefined,
|
||||
version: undefined,
|
||||
|
||||
// Docsearch specific
|
||||
type: 'lvl1',
|
||||
hierarchy: {
|
||||
lvl0: 'Guides',
|
||||
lvl1: title,
|
||||
lvl2: null,
|
||||
lvl3: null,
|
||||
lvl4: null,
|
||||
lvl5: null,
|
||||
lvl6: null,
|
||||
},
|
||||
}
|
||||
|
||||
return object
|
||||
})
|
||||
// Some of the reference generated files come with an 'index' page that we can ignore
|
||||
.filter((object) => !object.url.endsWith('/index'))
|
||||
.filter((object) => !object.url.endsWith('/.gitkeep'))
|
||||
|
||||
const combinedSearchObjects = guidePagesearchObjects.concat(
|
||||
clientLibSearchObjects,
|
||||
apiObjects,
|
||||
cliObjects
|
||||
)
|
||||
|
||||
//@ts-ignore
|
||||
await index.clearObjects()
|
||||
console.log(`Successfully cleared records from ${indexName}`)
|
||||
|
||||
//@ts-ignore
|
||||
const algoliaResponse = await index.saveObjects(combinedSearchObjects)
|
||||
|
||||
//@ts-ignore
|
||||
console.log(`Successfully saved ${algoliaResponse.objectIDs.length} records into ${indexName}.`)
|
||||
} catch (error) {
|
||||
console.log('Error:', error)
|
||||
}
|
||||
})()
|
||||
@@ -1,43 +0,0 @@
|
||||
import crypto from 'crypto'
|
||||
import { flattenSections } from '../../lib/helpers'
|
||||
|
||||
import apiCommonSections from '~/../../spec/common-api-sections.json'
|
||||
import specFile from '~/../../spec/transforms/api_v0_openapi_deparsed.json'
|
||||
import { gen_v3 } from '../../lib/refGenerator/helpers'
|
||||
|
||||
// @ts-ignore
|
||||
const generatedSpec = gen_v3(specFile, 'wat', { apiUrl: 'apiv0' })
|
||||
const sections = flattenSections(apiCommonSections)
|
||||
|
||||
export function generateAPISearchObjects() {
|
||||
let searchObjects = []
|
||||
|
||||
//@ts-ignore
|
||||
sections.map((section) => {
|
||||
const object = searchObjects.push({
|
||||
objectID: crypto.randomUUID(),
|
||||
id: section.id,
|
||||
title: section.title,
|
||||
// @ts-ignore
|
||||
description: generatedSpec.operations.find((item) => item.operationId === section.id)
|
||||
?.summary,
|
||||
url: `/reference/api/${section.slug}`,
|
||||
source: 'reference',
|
||||
pageContent: '',
|
||||
category: section.product,
|
||||
version: '',
|
||||
type: 'lvl2',
|
||||
hierarchy: {
|
||||
lvl0: 'References',
|
||||
lvl1: 'Management API',
|
||||
lvl2: section.title,
|
||||
lvl3: null,
|
||||
lvl4: null,
|
||||
lvl5: null,
|
||||
lvl6: null,
|
||||
},
|
||||
})
|
||||
return object
|
||||
})
|
||||
return searchObjects
|
||||
}
|
||||
@@ -1,43 +0,0 @@
|
||||
import crypto from 'crypto'
|
||||
import fs from 'fs'
|
||||
import { flattenSections } from '../../lib/helpers'
|
||||
import cliCommonSections from '~/../../spec/common-cli-sections.json'
|
||||
import yaml from 'js-yaml'
|
||||
|
||||
// @ts-ignore
|
||||
|
||||
const spec = yaml.load(fs.readFileSync(`../../spec/cli_v1_commands.yaml`, 'utf8'))
|
||||
|
||||
const commonSections = flattenSections(cliCommonSections)
|
||||
|
||||
export function generateCLISearchObjects() {
|
||||
let searchObjects = []
|
||||
|
||||
//@ts-ignore
|
||||
spec.commands.map((section) => {
|
||||
const object = searchObjects.push({
|
||||
objectID: crypto.randomUUID(),
|
||||
id: section.id,
|
||||
title: section.title,
|
||||
// @ts-ignore
|
||||
description: section.description?.substr(0, section.description.indexOf('\n')),
|
||||
url: `/reference/cli/${commonSections.find((item) => item.id === section.id)?.slug}`,
|
||||
source: 'reference',
|
||||
pageContent: '',
|
||||
category: section.product,
|
||||
version: '',
|
||||
type: 'lvl2',
|
||||
hierarchy: {
|
||||
lvl0: 'References',
|
||||
lvl1: 'Supabase CLI',
|
||||
lvl2: section.title,
|
||||
lvl3: null,
|
||||
lvl4: null,
|
||||
lvl5: null,
|
||||
lvl6: null,
|
||||
},
|
||||
})
|
||||
return object
|
||||
})
|
||||
return searchObjects
|
||||
}
|
||||
@@ -1,57 +0,0 @@
|
||||
import fs from 'fs'
|
||||
import crypto from 'crypto'
|
||||
import yaml from 'js-yaml'
|
||||
import { flattenSections } from '../../lib/helpers'
|
||||
import { nameMap } from '../helpers'
|
||||
|
||||
import commonLibSections from '~/../../spec/common-client-libs-sections.json'
|
||||
|
||||
const clientLibFiles = [
|
||||
{ fileName: 'supabase_js_v2', label: 'javascript', version: 'v2', versionSlug: false },
|
||||
{ fileName: 'supabase_js_v1', label: 'javascript', version: 'v1', versionSlug: true },
|
||||
{ fileName: 'supabase_dart_v1', label: 'dart', version: 'v1', versionSlug: false },
|
||||
{ fileName: 'supabase_dart_v0', label: 'dart', version: 'v0', versionSlug: true },
|
||||
{ fileName: 'supabase_dart_v0', label: 'dart', version: 'v0', versionSlug: true },
|
||||
]
|
||||
|
||||
const flatCommonLibSections = flattenSections(commonLibSections)
|
||||
|
||||
export function generateClientLibSearchObjects() {
|
||||
// loop through each spec file, find the correspending entry in the common file and grab the title / description / slug
|
||||
let clientLibSearchObjects = []
|
||||
|
||||
clientLibFiles.map((file) => {
|
||||
const specs = yaml.load(fs.readFileSync(`../../spec/${file.fileName}.yml`, 'utf8'))
|
||||
|
||||
//take each function id, find it in the commonLibSections file and return { id, title, slug, description, }
|
||||
//@ts-ignore
|
||||
specs.functions.map((fn) => {
|
||||
const item = flatCommonLibSections.find((section) => section.id === fn.id)
|
||||
if (item) {
|
||||
const object = clientLibSearchObjects.push({
|
||||
objectID: crypto.randomUUID(),
|
||||
id: item.id,
|
||||
title: item.title,
|
||||
description: item.title,
|
||||
url: `/reference/${file.label}/${file.versionSlug ? file.version + '/' : ''}${item.slug}`,
|
||||
source: 'reference',
|
||||
pageContent: '',
|
||||
category: item.product,
|
||||
version: file.version,
|
||||
type: 'lvl2',
|
||||
hierarchy: {
|
||||
lvl0: 'References',
|
||||
lvl1: `${nameMap[file.label]} ${file.version}`,
|
||||
lvl2: item.title,
|
||||
lvl3: file.version,
|
||||
lvl4: null,
|
||||
lvl5: null,
|
||||
lvl6: null,
|
||||
},
|
||||
})
|
||||
return object
|
||||
}
|
||||
})
|
||||
})
|
||||
return clientLibSearchObjects
|
||||
}
|
||||
@@ -1,659 +0,0 @@
|
||||
import { createClient } from '@supabase/supabase-js'
|
||||
import { createHash } from 'crypto'
|
||||
import dotenv from 'dotenv'
|
||||
import { ObjectExpression } from 'estree'
|
||||
import { readdir, readFile, stat } from 'fs/promises'
|
||||
import GithubSlugger from 'github-slugger'
|
||||
import yaml from 'js-yaml'
|
||||
import { Content, Root } from 'mdast'
|
||||
import { fromMarkdown } from 'mdast-util-from-markdown'
|
||||
import { mdxFromMarkdown, MdxjsEsm } from 'mdast-util-mdx'
|
||||
import { toMarkdown } from 'mdast-util-to-markdown'
|
||||
import { toString } from 'mdast-util-to-string'
|
||||
import { mdxjs } from 'micromark-extension-mdxjs'
|
||||
import 'openai'
|
||||
import { Configuration, OpenAIApi } from 'openai'
|
||||
import { OpenAPIV3 } from 'openapi-types'
|
||||
import { basename, dirname, join } from 'path'
|
||||
import { u } from 'unist-builder'
|
||||
import { filter } from 'unist-util-filter'
|
||||
import { inspect } from 'util'
|
||||
import { ICommonFunc, IFunctionDefinition, ISpec } from '../components/reference/Reference.types'
|
||||
import { CliCommand, CliSpec } from '../generator/types/CliSpec'
|
||||
import { flattenSections } from '../lib/helpers'
|
||||
import { enrichedOperation, gen_v3 } from '../lib/refGenerator/helpers'
|
||||
|
||||
dotenv.config()
|
||||
|
||||
const ignoredFiles = ['pages/404.mdx']
|
||||
|
||||
/**
|
||||
* Extracts ES literals from an `estree` `ObjectExpression`
|
||||
* into a plain JavaScript object.
|
||||
*/
|
||||
function getObjectFromExpression(node: ObjectExpression) {
|
||||
return node.properties.reduce<
|
||||
Record<string, string | number | bigint | true | RegExp | undefined>
|
||||
>((object, property) => {
|
||||
if (property.type !== 'Property') {
|
||||
return object
|
||||
}
|
||||
|
||||
const key = (property.key.type === 'Identifier' && property.key.name) || undefined
|
||||
const value = (property.value.type === 'Literal' && property.value.value) || undefined
|
||||
|
||||
if (!key) {
|
||||
return object
|
||||
}
|
||||
|
||||
return {
|
||||
...object,
|
||||
[key]: value,
|
||||
}
|
||||
}, {})
|
||||
}
|
||||
|
||||
/**
|
||||
* Extracts the `meta` ESM export from the MDX file.
|
||||
*
|
||||
* This info is akin to frontmatter.
|
||||
*/
|
||||
function extractMetaExport(mdxTree: Root) {
|
||||
const metaExportNode = mdxTree.children.find((node): node is MdxjsEsm => {
|
||||
return (
|
||||
node.type === 'mdxjsEsm' &&
|
||||
node.data?.estree?.body[0]?.type === 'ExportNamedDeclaration' &&
|
||||
node.data.estree.body[0].declaration?.type === 'VariableDeclaration' &&
|
||||
node.data.estree.body[0].declaration.declarations[0]?.id.type === 'Identifier' &&
|
||||
node.data.estree.body[0].declaration.declarations[0].id.name === 'meta'
|
||||
)
|
||||
})
|
||||
|
||||
if (!metaExportNode) {
|
||||
return undefined
|
||||
}
|
||||
|
||||
const objectExpression =
|
||||
(metaExportNode.data?.estree?.body[0]?.type === 'ExportNamedDeclaration' &&
|
||||
metaExportNode.data.estree.body[0].declaration?.type === 'VariableDeclaration' &&
|
||||
metaExportNode.data.estree.body[0].declaration.declarations[0]?.id.type === 'Identifier' &&
|
||||
metaExportNode.data.estree.body[0].declaration.declarations[0].id.name === 'meta' &&
|
||||
metaExportNode.data.estree.body[0].declaration.declarations[0].init?.type ===
|
||||
'ObjectExpression' &&
|
||||
metaExportNode.data.estree.body[0].declaration.declarations[0].init) ||
|
||||
undefined
|
||||
|
||||
if (!objectExpression) {
|
||||
return undefined
|
||||
}
|
||||
|
||||
return getObjectFromExpression(objectExpression)
|
||||
}
|
||||
|
||||
/**
|
||||
* Splits a `mdast` tree into multiple trees based on
|
||||
* a predicate function. Will include the splitting node
|
||||
* at the beginning of each tree.
|
||||
*
|
||||
* Useful to split a markdown file into smaller sections.
|
||||
*/
|
||||
function splitTreeBy(tree: Root, predicate: (node: Content) => boolean) {
|
||||
return tree.children.reduce<Root[]>((trees, node) => {
|
||||
const [lastTree] = trees.slice(-1)
|
||||
|
||||
if (!lastTree || predicate(node)) {
|
||||
const tree: Root = u('root', [node])
|
||||
return trees.concat(tree)
|
||||
}
|
||||
|
||||
lastTree.children.push(node)
|
||||
return trees
|
||||
}, [])
|
||||
}
|
||||
|
||||
type Meta = ReturnType<typeof extractMetaExport>
|
||||
|
||||
type Section = {
|
||||
content: string
|
||||
heading?: string
|
||||
slug?: string
|
||||
}
|
||||
|
||||
type ProcessedMdx = {
|
||||
checksum: string
|
||||
meta: Meta
|
||||
sections: Section[]
|
||||
}
|
||||
|
||||
/**
|
||||
* Processes MDX content for search indexing.
|
||||
* It extracts metadata, strips it of all JSX,
|
||||
* and splits it into sub-sections based on criteria.
|
||||
*/
|
||||
function processMdxForSearch(content: string): ProcessedMdx {
|
||||
const checksum = createHash('sha256').update(content).digest('base64')
|
||||
|
||||
const mdxTree = fromMarkdown(content, {
|
||||
extensions: [mdxjs()],
|
||||
mdastExtensions: [mdxFromMarkdown()],
|
||||
})
|
||||
|
||||
const meta = extractMetaExport(mdxTree)
|
||||
|
||||
// Remove all MDX elements from markdown
|
||||
const mdTree = filter(
|
||||
mdxTree,
|
||||
(node) =>
|
||||
![
|
||||
'mdxjsEsm',
|
||||
'mdxJsxFlowElement',
|
||||
'mdxJsxTextElement',
|
||||
'mdxFlowExpression',
|
||||
'mdxTextExpression',
|
||||
].includes(node.type)
|
||||
)
|
||||
|
||||
if (!mdTree) {
|
||||
return {
|
||||
checksum,
|
||||
meta,
|
||||
sections: [],
|
||||
}
|
||||
}
|
||||
|
||||
const sectionTrees = splitTreeBy(mdTree, (node) => node.type === 'heading')
|
||||
|
||||
const slugger = new GithubSlugger()
|
||||
|
||||
const sections = sectionTrees.map((tree) => {
|
||||
const [firstNode] = tree.children
|
||||
|
||||
const heading = firstNode.type === 'heading' ? toString(firstNode) : undefined
|
||||
const slug = heading ? slugger.slug(heading) : undefined
|
||||
|
||||
return {
|
||||
content: toMarkdown(tree),
|
||||
heading,
|
||||
slug,
|
||||
}
|
||||
})
|
||||
|
||||
return {
|
||||
checksum,
|
||||
meta,
|
||||
sections,
|
||||
}
|
||||
}
|
||||
|
||||
type WalkEntry = {
|
||||
path: string
|
||||
parentPath?: string
|
||||
}
|
||||
|
||||
async function walk(dir: string, parentPath?: string): Promise<WalkEntry[]> {
|
||||
const immediateFiles = await readdir(dir)
|
||||
|
||||
const recursiveFiles = await Promise.all(
|
||||
immediateFiles.map(async (file) => {
|
||||
const path = join(dir, file)
|
||||
const stats = await stat(path)
|
||||
if (stats.isDirectory()) {
|
||||
// Keep track of document hierarchy (if this dir has corresponding doc file)
|
||||
const docPath = `${basename(path)}.mdx`
|
||||
|
||||
return walk(
|
||||
path,
|
||||
immediateFiles.includes(docPath) ? join(dirname(path), docPath) : parentPath
|
||||
)
|
||||
} else if (stats.isFile()) {
|
||||
return [
|
||||
{
|
||||
path: path,
|
||||
parentPath,
|
||||
},
|
||||
]
|
||||
} else {
|
||||
return []
|
||||
}
|
||||
})
|
||||
)
|
||||
|
||||
const flattenedFiles = recursiveFiles.reduce(
|
||||
(all, folderContents) => all.concat(folderContents),
|
||||
[]
|
||||
)
|
||||
|
||||
return flattenedFiles.sort((a, b) => a.path.localeCompare(b.path))
|
||||
}
|
||||
|
||||
abstract class BaseEmbeddingSource {
|
||||
checksum?: string
|
||||
meta?: Meta
|
||||
sections?: Section[]
|
||||
|
||||
constructor(public source: string, public path: string, public parentPath?: string) {}
|
||||
|
||||
abstract load(): Promise<{ checksum: string; meta?: Meta; sections: Section[] }>
|
||||
}
|
||||
|
||||
class MarkdownEmbeddingSource extends BaseEmbeddingSource {
|
||||
type: 'markdown' = 'markdown'
|
||||
|
||||
constructor(source: string, public filePath: string, public parentFilePath?: string) {
|
||||
const path = filePath.replace(/^pages/, '').replace(/\.mdx?$/, '')
|
||||
const parentPath = parentFilePath?.replace(/^pages/, '').replace(/\.mdx?$/, '')
|
||||
|
||||
super(source, path, parentPath)
|
||||
}
|
||||
|
||||
async load() {
|
||||
const contents = await readFile(this.filePath, 'utf8')
|
||||
|
||||
const { checksum, meta, sections } = processMdxForSearch(contents)
|
||||
|
||||
this.checksum = checksum
|
||||
this.meta = meta
|
||||
this.sections = sections
|
||||
|
||||
return {
|
||||
checksum,
|
||||
meta,
|
||||
sections,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
abstract class ReferenceEmbeddingSource<SpecSection> extends BaseEmbeddingSource {
|
||||
type: 'reference' = 'reference'
|
||||
|
||||
constructor(
|
||||
source: string,
|
||||
path: string,
|
||||
public meta: Meta,
|
||||
public specFilePath: string,
|
||||
public sectionsFilePath: string
|
||||
) {
|
||||
super(source, path)
|
||||
}
|
||||
|
||||
async load() {
|
||||
const specContents = await readFile(this.specFilePath, 'utf8')
|
||||
const refSectionsContents = await readFile(this.sectionsFilePath, 'utf8')
|
||||
|
||||
const refSections: ICommonFunc[] = JSON.parse(refSectionsContents)
|
||||
const flattenedRefSections = flattenSections(refSections)
|
||||
|
||||
const checksum = createHash('sha256')
|
||||
.update(specContents + refSectionsContents)
|
||||
.digest('base64')
|
||||
|
||||
const specSections = this.getSpecSections(specContents)
|
||||
|
||||
const sections = flattenedRefSections
|
||||
.map((refSection) => {
|
||||
const specSection = this.matchSpecSection(specSections, refSection.id)
|
||||
|
||||
if (!specSection) {
|
||||
return
|
||||
}
|
||||
|
||||
return {
|
||||
heading: refSection.title,
|
||||
slug: refSection.slug,
|
||||
content: `${this.meta.title} for ${refSection.title}:\n${this.formatSection(
|
||||
specSection,
|
||||
refSection
|
||||
)}`,
|
||||
}
|
||||
})
|
||||
.filter((section) => !!section)
|
||||
|
||||
this.checksum = checksum
|
||||
this.sections = sections
|
||||
|
||||
return {
|
||||
checksum,
|
||||
sections,
|
||||
meta: this.meta,
|
||||
}
|
||||
}
|
||||
|
||||
abstract getSpecSections(specContents: string): SpecSection[]
|
||||
abstract matchSpecSection(specSections: SpecSection[], id: string): SpecSection
|
||||
abstract formatSection(specSection: SpecSection, refSection: ICommonFunc): string
|
||||
}
|
||||
|
||||
class OpenApiEmbeddingSource extends ReferenceEmbeddingSource<enrichedOperation> {
|
||||
getSpecSections(specContents: string): enrichedOperation[] {
|
||||
const spec: OpenAPIV3.Document<{}> = JSON.parse(specContents)
|
||||
|
||||
const generatedSpec = gen_v3(spec, '', {
|
||||
apiUrl: 'apiv0',
|
||||
})
|
||||
|
||||
return generatedSpec.operations
|
||||
}
|
||||
matchSpecSection(operations: enrichedOperation[], id: string): enrichedOperation {
|
||||
return operations.find((operation) => operation.operationId === id)
|
||||
}
|
||||
formatSection(specOperation: enrichedOperation) {
|
||||
const { summary, description, operation, path, tags } = specOperation
|
||||
return JSON.stringify({
|
||||
summary,
|
||||
description,
|
||||
operation,
|
||||
path,
|
||||
tags,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
class ClientLibEmbeddingSource extends ReferenceEmbeddingSource<IFunctionDefinition> {
|
||||
getSpecSections(specContents: string): IFunctionDefinition[] {
|
||||
const spec = yaml.load(specContents) as ISpec
|
||||
|
||||
return spec.functions
|
||||
}
|
||||
matchSpecSection(functionDefinitions: IFunctionDefinition[], id: string): IFunctionDefinition {
|
||||
return functionDefinitions.find((functionDefinition) => functionDefinition.id === id)
|
||||
}
|
||||
formatSection(functionDefinition: IFunctionDefinition, refSection: ICommonFunc): string {
|
||||
const { title } = refSection
|
||||
const { description, title: functionName } = functionDefinition
|
||||
|
||||
return JSON.stringify({
|
||||
title,
|
||||
description,
|
||||
functionName,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
class CliEmbeddingSource extends ReferenceEmbeddingSource<CliCommand> {
|
||||
getSpecSections(specContents: string): CliCommand[] {
|
||||
const spec = yaml.load(specContents) as CliSpec
|
||||
|
||||
return spec.commands
|
||||
}
|
||||
matchSpecSection(cliCommands: CliCommand[], id: string): CliCommand {
|
||||
return cliCommands.find((cliCommand) => cliCommand.id === id)
|
||||
}
|
||||
formatSection(cliCommand: CliCommand): string {
|
||||
const { summary, description, usage } = cliCommand
|
||||
return JSON.stringify({
|
||||
summary,
|
||||
description,
|
||||
usage,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
type EmbeddingSource =
|
||||
| MarkdownEmbeddingSource
|
||||
| OpenApiEmbeddingSource
|
||||
| ClientLibEmbeddingSource
|
||||
| CliEmbeddingSource
|
||||
|
||||
async function generateEmbeddings() {
|
||||
// TODO: use better CLI lib like yargs
|
||||
const args = process.argv.slice(2)
|
||||
const shouldRefresh = args.includes('--refresh')
|
||||
|
||||
if (
|
||||
!process.env.NEXT_PUBLIC_SUPABASE_URL ||
|
||||
!process.env.SUPABASE_SERVICE_ROLE_KEY ||
|
||||
!process.env.OPENAI_KEY
|
||||
) {
|
||||
return console.log(
|
||||
'Environment variables NEXT_PUBLIC_SUPABASE_URL, SUPABASE_SERVICE_ROLE_KEY, and OPENAI_KEY are required: skipping embeddings generation'
|
||||
)
|
||||
}
|
||||
|
||||
const supabaseClient = createClient(
|
||||
process.env.NEXT_PUBLIC_SUPABASE_URL,
|
||||
process.env.SUPABASE_SERVICE_ROLE_KEY,
|
||||
{
|
||||
auth: {
|
||||
persistSession: false,
|
||||
autoRefreshToken: false,
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
const embeddingSources: EmbeddingSource[] = [
|
||||
new OpenApiEmbeddingSource(
|
||||
'api',
|
||||
'/reference/api',
|
||||
{ title: 'Management API Reference' },
|
||||
'../../spec/transforms/api_v0_openapi_deparsed.json',
|
||||
'../../spec/common-api-sections.json'
|
||||
),
|
||||
new ClientLibEmbeddingSource(
|
||||
'js-lib',
|
||||
'/reference/javascript',
|
||||
{ title: 'JavaScript Reference' },
|
||||
'../../spec/supabase_js_v2.yml',
|
||||
'../../spec/common-client-libs-sections.json'
|
||||
),
|
||||
new ClientLibEmbeddingSource(
|
||||
'dart-lib',
|
||||
'/reference/dart',
|
||||
{ title: 'Dart Reference' },
|
||||
'../../spec/supabase_dart_v1.yml',
|
||||
'../../spec/common-client-libs-sections.json'
|
||||
),
|
||||
new ClientLibEmbeddingSource(
|
||||
'python-lib',
|
||||
'/reference/python',
|
||||
{ title: 'Python Reference' },
|
||||
'../../spec/supabase_py_v2.yml',
|
||||
'../../spec/common-client-libs-sections.json'
|
||||
),
|
||||
new ClientLibEmbeddingSource(
|
||||
'csharp-lib',
|
||||
'/reference/csharp',
|
||||
{ title: 'C# Reference' },
|
||||
'../../spec/supabase_csharp_v0.yml',
|
||||
'../../spec/common-client-libs-sections.json'
|
||||
),
|
||||
new CliEmbeddingSource(
|
||||
'cli',
|
||||
'/reference/cli',
|
||||
{ title: 'CLI Reference' },
|
||||
'../../spec/cli_v1_commands.yaml',
|
||||
'../../spec/common-cli-sections.json'
|
||||
),
|
||||
...(await walk('pages'))
|
||||
.filter(({ path }) => /\.mdx?$/.test(path))
|
||||
.filter(({ path }) => !ignoredFiles.includes(path))
|
||||
.map((entry) => new MarkdownEmbeddingSource('guide', entry.path)),
|
||||
]
|
||||
|
||||
console.log(`Discovered ${embeddingSources.length} pages`)
|
||||
|
||||
if (!shouldRefresh) {
|
||||
console.log('Checking which pages are new or have changed')
|
||||
} else {
|
||||
console.log('Refresh flag set, re-generating all pages')
|
||||
}
|
||||
|
||||
for (const embeddingSource of embeddingSources) {
|
||||
const { type, source, path, parentPath } = embeddingSource
|
||||
|
||||
try {
|
||||
const { checksum, meta, sections } = await embeddingSource.load()
|
||||
|
||||
// Check for existing page in DB and compare checksums
|
||||
const { error: fetchPageError, data: existingPage } = await supabaseClient
|
||||
.from('page')
|
||||
.select('id, path, checksum, parentPage:parent_page_id(id, path)')
|
||||
.filter('path', 'eq', path)
|
||||
.limit(1)
|
||||
.maybeSingle()
|
||||
|
||||
if (fetchPageError) {
|
||||
throw fetchPageError
|
||||
}
|
||||
|
||||
type Singular<T> = T extends any[] ? undefined : T
|
||||
|
||||
// We use checksum to determine if this page & its sections need to be regenerated
|
||||
if (!shouldRefresh && existingPage?.checksum === checksum) {
|
||||
const existingParentPage = existingPage?.parentPage as Singular<
|
||||
typeof existingPage.parentPage
|
||||
>
|
||||
|
||||
// If parent page changed, update it
|
||||
if (existingParentPage?.path !== parentPath) {
|
||||
console.log(`[${path}] Parent page has changed. Updating to '${parentPath}'...`)
|
||||
const { error: fetchParentPageError, data: parentPage } = await supabaseClient
|
||||
.from('page')
|
||||
.select()
|
||||
.filter('path', 'eq', parentPath)
|
||||
.limit(1)
|
||||
.maybeSingle()
|
||||
|
||||
if (fetchParentPageError) {
|
||||
throw fetchParentPageError
|
||||
}
|
||||
|
||||
const { error: updatePageError } = await supabaseClient
|
||||
.from('page')
|
||||
.update({ parent_page_id: parentPage?.id })
|
||||
.filter('id', 'eq', existingPage.id)
|
||||
|
||||
if (updatePageError) {
|
||||
throw updatePageError
|
||||
}
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
if (existingPage) {
|
||||
if (!shouldRefresh) {
|
||||
console.log(
|
||||
`[${path}] Docs have changed, removing old page sections and their embeddings`
|
||||
)
|
||||
} else {
|
||||
console.log(`[${path}] Refresh flag set, removing old page sections and their embeddings`)
|
||||
}
|
||||
|
||||
const { error: deletePageSectionError } = await supabaseClient
|
||||
.from('page_section')
|
||||
.delete()
|
||||
.filter('page_id', 'eq', existingPage.id)
|
||||
|
||||
if (deletePageSectionError) {
|
||||
throw deletePageSectionError
|
||||
}
|
||||
}
|
||||
|
||||
const { error: fetchParentPageError, data: parentPage } = await supabaseClient
|
||||
.from('page')
|
||||
.select()
|
||||
.filter('path', 'eq', parentPath)
|
||||
.limit(1)
|
||||
.maybeSingle()
|
||||
|
||||
if (fetchParentPageError) {
|
||||
throw fetchParentPageError
|
||||
}
|
||||
|
||||
// Create/update page record. Intentionally clear checksum until we
|
||||
// have successfully generated all page sections.
|
||||
const { error: upsertPageError, data: page } = await supabaseClient
|
||||
.from('page')
|
||||
.upsert(
|
||||
{
|
||||
checksum: null,
|
||||
path,
|
||||
type,
|
||||
source,
|
||||
meta,
|
||||
parent_page_id: parentPage?.id,
|
||||
},
|
||||
{ onConflict: 'path' }
|
||||
)
|
||||
.select()
|
||||
.limit(1)
|
||||
.single()
|
||||
|
||||
if (upsertPageError) {
|
||||
throw upsertPageError
|
||||
}
|
||||
|
||||
console.log(`[${path}] Adding ${sections.length} page sections (with embeddings)`)
|
||||
for (const { slug, heading, content } of sections) {
|
||||
// OpenAI recommends replacing newlines with spaces for best results (specific to embeddings)
|
||||
const input = content.replace(/\n/g, ' ')
|
||||
|
||||
try {
|
||||
const configuration = new Configuration({ apiKey: process.env.OPENAI_KEY })
|
||||
const openai = new OpenAIApi(configuration)
|
||||
|
||||
const embeddingResponse = await openai.createEmbedding({
|
||||
model: 'text-embedding-ada-002',
|
||||
input,
|
||||
})
|
||||
|
||||
if (embeddingResponse.status !== 200) {
|
||||
throw new Error(inspect(embeddingResponse.data, false, 2))
|
||||
}
|
||||
|
||||
const [responseData] = embeddingResponse.data.data
|
||||
|
||||
const { error: insertPageSectionError, data: pageSection } = await supabaseClient
|
||||
.from('page_section')
|
||||
.insert({
|
||||
page_id: page.id,
|
||||
slug,
|
||||
heading,
|
||||
content,
|
||||
token_count: embeddingResponse.data.usage.total_tokens,
|
||||
embedding: responseData.embedding,
|
||||
})
|
||||
.select()
|
||||
.limit(1)
|
||||
.single()
|
||||
|
||||
if (insertPageSectionError) {
|
||||
throw insertPageSectionError
|
||||
}
|
||||
} catch (err) {
|
||||
// TODO: decide how to better handle failed embeddings
|
||||
console.error(
|
||||
`Failed to generate embeddings for '${path}' page section starting with '${input.slice(
|
||||
0,
|
||||
40
|
||||
)}...'`
|
||||
)
|
||||
|
||||
throw err
|
||||
}
|
||||
}
|
||||
|
||||
// Set page checksum so that we know this page was stored successfully
|
||||
const { error: updatePageError } = await supabaseClient
|
||||
.from('page')
|
||||
.update({ checksum })
|
||||
.filter('id', 'eq', page.id)
|
||||
|
||||
if (updatePageError) {
|
||||
throw updatePageError
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(
|
||||
`Page '${path}' or one/multiple of its page sections failed to store properly. Page has been marked with null checksum to indicate that it needs to be re-generated.`
|
||||
)
|
||||
console.error(err)
|
||||
}
|
||||
}
|
||||
|
||||
console.log('Embedding generation complete')
|
||||
}
|
||||
|
||||
async function main() {
|
||||
await generateEmbeddings()
|
||||
}
|
||||
|
||||
main().catch((err) => console.error(err))
|
||||
@@ -1,9 +0,0 @@
|
||||
export const nameMap = {
|
||||
api: 'Management API',
|
||||
cli: 'Supabase CLI',
|
||||
auth: 'Auth Server',
|
||||
storage: 'Storage Server',
|
||||
postgres: 'Postgres',
|
||||
dart: 'Supabase Flutter Library',
|
||||
javascript: 'Supabase JavaScript Library',
|
||||
}
|
||||
@@ -0,0 +1,225 @@
|
||||
import { createClient } from '@supabase/supabase-js'
|
||||
import dotenv from 'dotenv'
|
||||
import 'openai'
|
||||
import { Configuration, OpenAIApi } from 'openai'
|
||||
import { inspect } from 'util'
|
||||
import { fetchSources } from './sources'
|
||||
|
||||
dotenv.config()
|
||||
|
||||
async function generateEmbeddings() {
|
||||
// TODO: use better CLI lib like yargs
|
||||
const args = process.argv.slice(2)
|
||||
const shouldRefresh = args.includes('--refresh')
|
||||
|
||||
const requiredEnvVars = ['NEXT_PUBLIC_SUPABASE_URL', 'SUPABASE_SERVICE_ROLE_KEY', 'OPENAI_KEY']
|
||||
|
||||
if (requiredEnvVars.some((name) => !process.env[name])) {
|
||||
throw new Error(
|
||||
`Environment variables ${requiredEnvVars.join(
|
||||
', '
|
||||
)} are required: skipping embeddings generation`
|
||||
)
|
||||
}
|
||||
|
||||
const supabaseClient = createClient(
|
||||
process.env.NEXT_PUBLIC_SUPABASE_URL,
|
||||
process.env.SUPABASE_SERVICE_ROLE_KEY,
|
||||
{
|
||||
auth: {
|
||||
persistSession: false,
|
||||
autoRefreshToken: false,
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
const embeddingSources = await fetchSources()
|
||||
|
||||
console.log(`Discovered ${embeddingSources.length} pages`)
|
||||
|
||||
if (!shouldRefresh) {
|
||||
console.log('Checking which pages are new or have changed')
|
||||
} else {
|
||||
console.log('Refresh flag set, re-generating all pages')
|
||||
}
|
||||
|
||||
for (const embeddingSource of embeddingSources) {
|
||||
const { type, source, path, parentPath } = embeddingSource
|
||||
|
||||
try {
|
||||
const { checksum, meta, sections } = await embeddingSource.load()
|
||||
|
||||
// Check for existing page in DB and compare checksums
|
||||
const { error: fetchPageError, data: existingPage } = await supabaseClient
|
||||
.from('page')
|
||||
.select('id, path, checksum, parentPage:parent_page_id(id, path)')
|
||||
.filter('path', 'eq', path)
|
||||
.limit(1)
|
||||
.maybeSingle()
|
||||
|
||||
if (fetchPageError) {
|
||||
throw fetchPageError
|
||||
}
|
||||
|
||||
type Singular<T> = T extends any[] ? undefined : T
|
||||
|
||||
// We use checksum to determine if this page & its sections need to be regenerated
|
||||
if (!shouldRefresh && existingPage?.checksum === checksum) {
|
||||
const existingParentPage = existingPage?.parentPage as Singular<
|
||||
typeof existingPage.parentPage
|
||||
>
|
||||
|
||||
// If parent page changed, update it
|
||||
if (existingParentPage?.path !== parentPath) {
|
||||
console.log(`[${path}] Parent page has changed. Updating to '${parentPath}'...`)
|
||||
const { error: fetchParentPageError, data: parentPage } = await supabaseClient
|
||||
.from('page')
|
||||
.select()
|
||||
.filter('path', 'eq', parentPath)
|
||||
.limit(1)
|
||||
.maybeSingle()
|
||||
|
||||
if (fetchParentPageError) {
|
||||
throw fetchParentPageError
|
||||
}
|
||||
|
||||
const { error: updatePageError } = await supabaseClient
|
||||
.from('page')
|
||||
.update({ parent_page_id: parentPage?.id })
|
||||
.filter('id', 'eq', existingPage.id)
|
||||
|
||||
if (updatePageError) {
|
||||
throw updatePageError
|
||||
}
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
if (existingPage) {
|
||||
if (!shouldRefresh) {
|
||||
console.log(
|
||||
`[${path}] Docs have changed, removing old page sections and their embeddings`
|
||||
)
|
||||
} else {
|
||||
console.log(`[${path}] Refresh flag set, removing old page sections and their embeddings`)
|
||||
}
|
||||
|
||||
const { error: deletePageSectionError } = await supabaseClient
|
||||
.from('page_section')
|
||||
.delete()
|
||||
.filter('page_id', 'eq', existingPage.id)
|
||||
|
||||
if (deletePageSectionError) {
|
||||
throw deletePageSectionError
|
||||
}
|
||||
}
|
||||
|
||||
const { error: fetchParentPageError, data: parentPage } = await supabaseClient
|
||||
.from('page')
|
||||
.select()
|
||||
.filter('path', 'eq', parentPath)
|
||||
.limit(1)
|
||||
.maybeSingle()
|
||||
|
||||
if (fetchParentPageError) {
|
||||
throw fetchParentPageError
|
||||
}
|
||||
|
||||
// Create/update page record. Intentionally clear checksum until we
|
||||
// have successfully generated all page sections.
|
||||
const { error: upsertPageError, data: page } = await supabaseClient
|
||||
.from('page')
|
||||
.upsert(
|
||||
{
|
||||
checksum: null,
|
||||
path,
|
||||
type,
|
||||
source,
|
||||
meta,
|
||||
parent_page_id: parentPage?.id,
|
||||
},
|
||||
{ onConflict: 'path' }
|
||||
)
|
||||
.select()
|
||||
.limit(1)
|
||||
.single()
|
||||
|
||||
if (upsertPageError) {
|
||||
throw upsertPageError
|
||||
}
|
||||
|
||||
console.log(`[${path}] Adding ${sections.length} page sections (with embeddings)`)
|
||||
for (const { slug, heading, content } of sections) {
|
||||
// OpenAI recommends replacing newlines with spaces for best results (specific to embeddings)
|
||||
const input = content.replace(/\n/g, ' ')
|
||||
|
||||
try {
|
||||
const configuration = new Configuration({ apiKey: process.env.OPENAI_KEY })
|
||||
const openai = new OpenAIApi(configuration)
|
||||
|
||||
const embeddingResponse = await openai.createEmbedding({
|
||||
model: 'text-embedding-ada-002',
|
||||
input,
|
||||
})
|
||||
|
||||
if (embeddingResponse.status !== 200) {
|
||||
throw new Error(inspect(embeddingResponse.data, false, 2))
|
||||
}
|
||||
|
||||
const [responseData] = embeddingResponse.data.data
|
||||
|
||||
const { error: insertPageSectionError, data: pageSection } = await supabaseClient
|
||||
.from('page_section')
|
||||
.insert({
|
||||
page_id: page.id,
|
||||
slug,
|
||||
heading,
|
||||
content,
|
||||
token_count: embeddingResponse.data.usage.total_tokens,
|
||||
embedding: responseData.embedding,
|
||||
})
|
||||
.select()
|
||||
.limit(1)
|
||||
.single()
|
||||
|
||||
if (insertPageSectionError) {
|
||||
throw insertPageSectionError
|
||||
}
|
||||
} catch (err) {
|
||||
// TODO: decide how to better handle failed embeddings
|
||||
console.error(
|
||||
`Failed to generate embeddings for '${path}' page section starting with '${input.slice(
|
||||
0,
|
||||
40
|
||||
)}...'`
|
||||
)
|
||||
|
||||
throw err
|
||||
}
|
||||
}
|
||||
|
||||
// Set page checksum so that we know this page was stored successfully
|
||||
const { error: updatePageError } = await supabaseClient
|
||||
.from('page')
|
||||
.update({ checksum })
|
||||
.filter('id', 'eq', page.id)
|
||||
|
||||
if (updatePageError) {
|
||||
throw updatePageError
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(
|
||||
`Page '${path}' or one/multiple of its page sections failed to store properly. Page has been marked with null checksum to indicate that it needs to be re-generated.`
|
||||
)
|
||||
console.error(err)
|
||||
}
|
||||
}
|
||||
|
||||
console.log('Embedding generation complete')
|
||||
}
|
||||
|
||||
async function main() {
|
||||
await generateEmbeddings()
|
||||
}
|
||||
|
||||
main().catch((err) => console.error(err))
|
||||
@@ -0,0 +1,20 @@
|
||||
export type Json = Record<
|
||||
string,
|
||||
string | number | boolean | null | Json[] | { [key: string]: Json }
|
||||
>
|
||||
|
||||
export type Section = {
|
||||
content: string
|
||||
heading?: string
|
||||
slug?: string
|
||||
}
|
||||
|
||||
export abstract class BaseSource {
|
||||
checksum?: string
|
||||
meta?: Json
|
||||
sections?: Section[]
|
||||
|
||||
constructor(public source: string, public path: string, public parentPath?: string) {}
|
||||
|
||||
abstract load(): Promise<{ checksum: string; meta?: Json; sections: Section[] }>
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
import { MarkdownSource } from './markdown'
|
||||
import {
|
||||
CliReferenceSource,
|
||||
ClientLibReferenceSource,
|
||||
OpenApiReferenceSource,
|
||||
} from './reference-doc'
|
||||
import { walk } from './util'
|
||||
|
||||
const ignoredFiles = ['pages/404.mdx']
|
||||
|
||||
export type SearchSource =
|
||||
| MarkdownSource
|
||||
| OpenApiReferenceSource
|
||||
| ClientLibReferenceSource
|
||||
| CliReferenceSource
|
||||
|
||||
/**
|
||||
* Fetches all the sources we want to index for search
|
||||
*/
|
||||
export async function fetchSources() {
|
||||
const openApiReferenceSource = new OpenApiReferenceSource(
|
||||
'api',
|
||||
'/reference/api',
|
||||
{ title: 'Management API Reference' },
|
||||
'../../spec/transforms/api_v0_openapi_deparsed.json',
|
||||
'../../spec/common-api-sections.json'
|
||||
)
|
||||
|
||||
const jsLibReferenceSource = new ClientLibReferenceSource(
|
||||
'js-lib',
|
||||
'/reference/javascript',
|
||||
{ title: 'JavaScript Reference' },
|
||||
'../../spec/supabase_js_v2.yml',
|
||||
'../../spec/common-client-libs-sections.json'
|
||||
)
|
||||
|
||||
const dartLibReferenceSource = new ClientLibReferenceSource(
|
||||
'dart-lib',
|
||||
'/reference/dart',
|
||||
{ title: 'Dart Reference' },
|
||||
'../../spec/supabase_dart_v1.yml',
|
||||
'../../spec/common-client-libs-sections.json'
|
||||
)
|
||||
|
||||
const pythonLibReferenceSource = new ClientLibReferenceSource(
|
||||
'python-lib',
|
||||
'/reference/python',
|
||||
{ title: 'Python Reference' },
|
||||
'../../spec/supabase_py_v2.yml',
|
||||
'../../spec/common-client-libs-sections.json'
|
||||
)
|
||||
|
||||
const cSharpLibReferenceSource = new ClientLibReferenceSource(
|
||||
'csharp-lib',
|
||||
'/reference/csharp',
|
||||
{ title: 'C# Reference' },
|
||||
'../../spec/supabase_csharp_v0.yml',
|
||||
'../../spec/common-client-libs-sections.json'
|
||||
)
|
||||
|
||||
const cliReferenceSource = new CliReferenceSource(
|
||||
'cli',
|
||||
'/reference/cli',
|
||||
{ title: 'CLI Reference' },
|
||||
'../../spec/cli_v1_commands.yaml',
|
||||
'../../spec/common-cli-sections.json'
|
||||
)
|
||||
|
||||
const guideSources = (await walk('pages'))
|
||||
.filter(({ path }) => /\.mdx?$/.test(path))
|
||||
.filter(({ path }) => !ignoredFiles.includes(path))
|
||||
.map((entry) => new MarkdownSource('guide', entry.path))
|
||||
|
||||
const sources: SearchSource[] = [
|
||||
openApiReferenceSource,
|
||||
jsLibReferenceSource,
|
||||
dartLibReferenceSource,
|
||||
pythonLibReferenceSource,
|
||||
cSharpLibReferenceSource,
|
||||
cliReferenceSource,
|
||||
...guideSources,
|
||||
]
|
||||
|
||||
return sources
|
||||
}
|
||||
@@ -0,0 +1,191 @@
|
||||
import { createHash } from 'crypto'
|
||||
import { ObjectExpression } from 'estree'
|
||||
import { readFile } from 'fs/promises'
|
||||
import GithubSlugger from 'github-slugger'
|
||||
import { Content, Root } from 'mdast'
|
||||
import { fromMarkdown } from 'mdast-util-from-markdown'
|
||||
import { MdxjsEsm, mdxFromMarkdown } from 'mdast-util-mdx'
|
||||
import { toMarkdown } from 'mdast-util-to-markdown'
|
||||
import { toString } from 'mdast-util-to-string'
|
||||
import { mdxjs } from 'micromark-extension-mdxjs'
|
||||
import { u } from 'unist-builder'
|
||||
import { filter } from 'unist-util-filter'
|
||||
import { BaseSource, Json, Section } from './base'
|
||||
|
||||
/**
|
||||
* Extracts ES literals from an `estree` `ObjectExpression`
|
||||
* into a plain JavaScript object.
|
||||
*/
|
||||
export function getObjectFromExpression(node: ObjectExpression) {
|
||||
return node.properties.reduce<
|
||||
Record<string, string | number | bigint | true | RegExp | undefined>
|
||||
>((object, property) => {
|
||||
if (property.type !== 'Property') {
|
||||
return object
|
||||
}
|
||||
|
||||
const key = (property.key.type === 'Identifier' && property.key.name) || undefined
|
||||
const value = (property.value.type === 'Literal' && property.value.value) || undefined
|
||||
|
||||
if (!key) {
|
||||
return object
|
||||
}
|
||||
|
||||
return {
|
||||
...object,
|
||||
[key]: value,
|
||||
}
|
||||
}, {})
|
||||
}
|
||||
|
||||
/**
|
||||
* Extracts the `meta` ESM export from the MDX file.
|
||||
*
|
||||
* This info is akin to frontmatter.
|
||||
*/
|
||||
export function extractMetaExport(mdxTree: Root) {
|
||||
const metaExportNode = mdxTree.children.find((node): node is MdxjsEsm => {
|
||||
return (
|
||||
node.type === 'mdxjsEsm' &&
|
||||
node.data?.estree?.body[0]?.type === 'ExportNamedDeclaration' &&
|
||||
node.data.estree.body[0].declaration?.type === 'VariableDeclaration' &&
|
||||
node.data.estree.body[0].declaration.declarations[0]?.id.type === 'Identifier' &&
|
||||
node.data.estree.body[0].declaration.declarations[0].id.name === 'meta'
|
||||
)
|
||||
})
|
||||
|
||||
if (!metaExportNode) {
|
||||
return undefined
|
||||
}
|
||||
|
||||
const objectExpression =
|
||||
(metaExportNode.data?.estree?.body[0]?.type === 'ExportNamedDeclaration' &&
|
||||
metaExportNode.data.estree.body[0].declaration?.type === 'VariableDeclaration' &&
|
||||
metaExportNode.data.estree.body[0].declaration.declarations[0]?.id.type === 'Identifier' &&
|
||||
metaExportNode.data.estree.body[0].declaration.declarations[0].id.name === 'meta' &&
|
||||
metaExportNode.data.estree.body[0].declaration.declarations[0].init?.type ===
|
||||
'ObjectExpression' &&
|
||||
metaExportNode.data.estree.body[0].declaration.declarations[0].init) ||
|
||||
undefined
|
||||
|
||||
if (!objectExpression) {
|
||||
return undefined
|
||||
}
|
||||
|
||||
return getObjectFromExpression(objectExpression)
|
||||
}
|
||||
|
||||
/**
|
||||
* Splits a `mdast` tree into multiple trees based on
|
||||
* a predicate function. Will include the splitting node
|
||||
* at the beginning of each tree.
|
||||
*
|
||||
* Useful to split a markdown file into smaller sections.
|
||||
*/
|
||||
export function splitTreeBy(tree: Root, predicate: (node: Content) => boolean) {
|
||||
return tree.children.reduce<Root[]>((trees, node) => {
|
||||
const [lastTree] = trees.slice(-1)
|
||||
|
||||
if (!lastTree || predicate(node)) {
|
||||
const tree: Root = u('root', [node])
|
||||
return trees.concat(tree)
|
||||
}
|
||||
|
||||
lastTree.children.push(node)
|
||||
return trees
|
||||
}, [])
|
||||
}
|
||||
|
||||
/**
|
||||
* Processes MDX content for search indexing.
|
||||
* It extracts metadata, strips it of all JSX,
|
||||
* and splits it into sub-sections based on criteria.
|
||||
*/
|
||||
export function processMdxForSearch(content: string): ProcessedMdx {
|
||||
const checksum = createHash('sha256').update(content).digest('base64')
|
||||
|
||||
const mdxTree = fromMarkdown(content, {
|
||||
extensions: [mdxjs()],
|
||||
mdastExtensions: [mdxFromMarkdown()],
|
||||
})
|
||||
|
||||
const meta = extractMetaExport(mdxTree)
|
||||
const serializableMeta: Json = JSON.parse(JSON.stringify(meta))
|
||||
|
||||
// Remove all MDX elements from markdown
|
||||
const mdTree = filter(
|
||||
mdxTree,
|
||||
(node) =>
|
||||
![
|
||||
'mdxjsEsm',
|
||||
'mdxJsxFlowElement',
|
||||
'mdxJsxTextElement',
|
||||
'mdxFlowExpression',
|
||||
'mdxTextExpression',
|
||||
].includes(node.type)
|
||||
)
|
||||
|
||||
if (!mdTree) {
|
||||
return {
|
||||
checksum,
|
||||
meta: serializableMeta,
|
||||
sections: [],
|
||||
}
|
||||
}
|
||||
|
||||
const sectionTrees = splitTreeBy(mdTree, (node) => node.type === 'heading')
|
||||
|
||||
const slugger = new GithubSlugger()
|
||||
|
||||
const sections = sectionTrees.map((tree) => {
|
||||
const [firstNode] = tree.children
|
||||
|
||||
const heading = firstNode.type === 'heading' ? toString(firstNode) : undefined
|
||||
const slug = heading ? slugger.slug(heading) : undefined
|
||||
|
||||
return {
|
||||
content: toMarkdown(tree),
|
||||
heading,
|
||||
slug,
|
||||
}
|
||||
})
|
||||
|
||||
return {
|
||||
checksum,
|
||||
meta: serializableMeta,
|
||||
sections,
|
||||
}
|
||||
}
|
||||
|
||||
export type ProcessedMdx = {
|
||||
checksum: string
|
||||
meta: Json
|
||||
sections: Section[]
|
||||
}
|
||||
|
||||
export class MarkdownSource extends BaseSource {
|
||||
type = 'markdown' as const
|
||||
|
||||
constructor(source: string, public filePath: string, public parentFilePath?: string) {
|
||||
const path = filePath.replace(/^pages/, '').replace(/\.mdx?$/, '')
|
||||
const parentPath = parentFilePath?.replace(/^pages/, '').replace(/\.mdx?$/, '')
|
||||
|
||||
super(source, path, parentPath)
|
||||
}
|
||||
|
||||
async load() {
|
||||
const contents = await readFile(this.filePath, 'utf8')
|
||||
|
||||
const { checksum, meta, sections } = processMdxForSearch(contents)
|
||||
|
||||
this.checksum = checksum
|
||||
this.meta = meta
|
||||
this.sections = sections
|
||||
|
||||
return {
|
||||
checksum,
|
||||
meta,
|
||||
sections,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,138 @@
|
||||
import { createHash } from 'crypto'
|
||||
import { readFile } from 'fs/promises'
|
||||
import yaml from 'js-yaml'
|
||||
import { OpenAPIV3 } from 'openapi-types'
|
||||
import {
|
||||
ICommonFunc,
|
||||
IFunctionDefinition,
|
||||
ISpec,
|
||||
} from '../../../components/reference/Reference.types'
|
||||
import { CliCommand, CliSpec } from '../../../generator/types/CliSpec'
|
||||
import { flattenSections } from '../../../lib/helpers'
|
||||
import { enrichedOperation, gen_v3 } from '../../../lib/refGenerator/helpers'
|
||||
import { BaseSource, Json } from './base'
|
||||
|
||||
export abstract class ReferenceSource<SpecSection> extends BaseSource {
|
||||
type = 'reference' as const
|
||||
|
||||
constructor(
|
||||
source: string,
|
||||
path: string,
|
||||
public meta: Json,
|
||||
public specFilePath: string,
|
||||
public sectionsFilePath: string
|
||||
) {
|
||||
super(source, path)
|
||||
}
|
||||
|
||||
async load() {
|
||||
const specContents = await readFile(this.specFilePath, 'utf8')
|
||||
const refSectionsContents = await readFile(this.sectionsFilePath, 'utf8')
|
||||
|
||||
const refSections: ICommonFunc[] = JSON.parse(refSectionsContents)
|
||||
const flattenedRefSections = flattenSections(refSections)
|
||||
|
||||
const checksum = createHash('sha256')
|
||||
.update(specContents + refSectionsContents)
|
||||
.digest('base64')
|
||||
|
||||
const specSections = this.getSpecSections(specContents)
|
||||
|
||||
const sections = flattenedRefSections
|
||||
.map((refSection) => {
|
||||
const specSection = this.matchSpecSection(specSections, refSection.id)
|
||||
|
||||
if (!specSection) {
|
||||
return
|
||||
}
|
||||
|
||||
return {
|
||||
heading: refSection.title,
|
||||
slug: refSection.slug,
|
||||
content: `${this.meta.title} for ${refSection.title}:\n${this.formatSection(
|
||||
specSection,
|
||||
refSection
|
||||
)}`,
|
||||
}
|
||||
})
|
||||
.filter((section) => !!section)
|
||||
|
||||
this.checksum = checksum
|
||||
this.sections = sections
|
||||
|
||||
return {
|
||||
checksum,
|
||||
sections,
|
||||
meta: this.meta,
|
||||
}
|
||||
}
|
||||
|
||||
abstract getSpecSections(specContents: string): SpecSection[]
|
||||
abstract matchSpecSection(specSections: SpecSection[], id: string): SpecSection
|
||||
abstract formatSection(specSection: SpecSection, refSection: ICommonFunc): string
|
||||
}
|
||||
|
||||
export class OpenApiReferenceSource extends ReferenceSource<enrichedOperation> {
|
||||
getSpecSections(specContents: string): enrichedOperation[] {
|
||||
const spec: OpenAPIV3.Document<{}> = JSON.parse(specContents)
|
||||
|
||||
const generatedSpec = gen_v3(spec, '', {
|
||||
apiUrl: 'apiv0',
|
||||
})
|
||||
|
||||
return generatedSpec.operations
|
||||
}
|
||||
matchSpecSection(operations: enrichedOperation[], id: string): enrichedOperation {
|
||||
return operations.find((operation) => operation.operationId === id)
|
||||
}
|
||||
formatSection(specOperation: enrichedOperation) {
|
||||
const { summary, description, operation, path, tags } = specOperation
|
||||
return JSON.stringify({
|
||||
summary,
|
||||
description,
|
||||
operation,
|
||||
path,
|
||||
tags,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
export class ClientLibReferenceSource extends ReferenceSource<IFunctionDefinition> {
|
||||
getSpecSections(specContents: string): IFunctionDefinition[] {
|
||||
const spec = yaml.load(specContents) as ISpec
|
||||
|
||||
return spec.functions
|
||||
}
|
||||
matchSpecSection(functionDefinitions: IFunctionDefinition[], id: string): IFunctionDefinition {
|
||||
return functionDefinitions.find((functionDefinition) => functionDefinition.id === id)
|
||||
}
|
||||
formatSection(functionDefinition: IFunctionDefinition, refSection: ICommonFunc): string {
|
||||
const { title } = refSection
|
||||
const { description, title: functionName } = functionDefinition
|
||||
|
||||
return JSON.stringify({
|
||||
title,
|
||||
description,
|
||||
functionName,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
export class CliReferenceSource extends ReferenceSource<CliCommand> {
|
||||
getSpecSections(specContents: string): CliCommand[] {
|
||||
const spec = yaml.load(specContents) as CliSpec
|
||||
|
||||
return spec.commands
|
||||
}
|
||||
matchSpecSection(cliCommands: CliCommand[], id: string): CliCommand {
|
||||
return cliCommands.find((cliCommand) => cliCommand.id === id)
|
||||
}
|
||||
formatSection(cliCommand: CliCommand): string {
|
||||
const { summary, description, usage } = cliCommand
|
||||
return JSON.stringify({
|
||||
summary,
|
||||
description,
|
||||
usage,
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
import { readdir, stat } from 'fs/promises'
|
||||
import { basename, dirname, join } from 'path'
|
||||
|
||||
export type WalkEntry = {
|
||||
path: string
|
||||
parentPath?: string
|
||||
}
|
||||
|
||||
export async function walk(dir: string, parentPath?: string): Promise<WalkEntry[]> {
|
||||
const immediateFiles = await readdir(dir)
|
||||
|
||||
const recursiveFiles = await Promise.all(
|
||||
immediateFiles.map(async (file) => {
|
||||
const path = join(dir, file)
|
||||
const stats = await stat(path)
|
||||
if (stats.isDirectory()) {
|
||||
// Keep track of document hierarchy (if this dir has corresponding doc file)
|
||||
const docPath = `${basename(path)}.mdx`
|
||||
|
||||
return walk(
|
||||
path,
|
||||
immediateFiles.includes(docPath) ? join(dirname(path), docPath) : parentPath
|
||||
)
|
||||
} else if (stats.isFile()) {
|
||||
return [
|
||||
{
|
||||
path: path,
|
||||
parentPath,
|
||||
},
|
||||
]
|
||||
} else {
|
||||
return []
|
||||
}
|
||||
})
|
||||
)
|
||||
|
||||
const flattenedFiles = recursiveFiles.reduce(
|
||||
(all, folderContents) => all.concat(folderContents),
|
||||
[]
|
||||
)
|
||||
|
||||
return flattenedFiles.sort((a, b) => a.path.localeCompare(b.path))
|
||||
}
|
||||
@@ -15,7 +15,16 @@ date: '2021-12-03'
|
||||
toc_depth: 3
|
||||
---
|
||||
|
||||
Today we're open sourcing [`pg_graphql`](https://github.com/supabase/pg_graphql), a work-in-progress native PostgreSQL extension adding GraphQL support. The extension keeps schema generation, query parsing, and resolvers all neatly contained on your database server requiring no external services.
|
||||
<div className="bg-gray-300 rounded-lg px-6 py-2 italic">
|
||||
|
||||
🆕 `pg_graphql` is now generally available and has undergone significant enhancements since this announcement. Here is what is new:
|
||||
|
||||
- [pg_graphql v1.0](https://supabase.com/blog/pg-graphql-v1)
|
||||
- [New Features in pg_graphql v1.2](https://supabase.com/blog/whats-new-in-pg-graphql-v1-2)
|
||||
|
||||
</div>
|
||||
|
||||
Today we're open sourcing [`pg_graphql`](https://github.com/supabase/pg_graphql), a native PostgreSQL extension adding GraphQL support. The extension keeps schema generation, query parsing, and resolvers all neatly contained on your database server requiring no external services.
|
||||
|
||||
`pg_graphql` inspects an existing PostgreSQL schema and reflects a GraphQL schema with resolvers that are:
|
||||
|
||||
@@ -27,6 +36,7 @@ Today we're open sourcing [`pg_graphql`](https://github.com/supabase/pg_graphql)
|
||||
|
||||
Interested? You're [3 commands away](https://supabase.github.io/pg_graphql/quickstart/) from a live [GraphiQL](https://graphql-dotnet.github.io/docs/getting-started/graphiql/) demo.
|
||||
|
||||
|
||||
## Motivation
|
||||
|
||||
The Supabase stack is centered around PostgreSQL as the single source of truth. All data, configuration, and security are housed in the database so any GraphQL solution needed to be equivalently SQL-centric.
|
||||
@@ -190,3 +200,8 @@ Full steps to reproduce this output are available in [the docs](https://supabase
|
||||
`pg_graphql` is [open source software](https://github.com/supabase/pg_graphql/). As always, Issues and PRs are welcome.
|
||||
|
||||
[Try `pg_graphql`](https://supabase.github.io/pg_graphql/quickstart/) today to see a live [GraphiQL](https://graphql-dotnet.github.io/docs/getting-started/graphiql/) demo.
|
||||
|
||||
## More pg_graphql
|
||||
|
||||
- [pg_graphql v1.0](https://supabase.com/blog/pg-graphql-v1)
|
||||
- [New Features in pg_graphql v1.2](https://supabase.com/blog/whats-new-in-pg-graphql-v1-2)
|
||||
@@ -1,6 +1,6 @@
|
||||
---
|
||||
title: 'GraphQL is now available in Supabase'
|
||||
description: GraphQL support is now in general availability on the Supabase platform via our open source PostgreSQL extension, pg_graphql (beta).
|
||||
description: GraphQL support is now in general availability on the Supabase platform via our open source PostgreSQL extension, pg_graphql.
|
||||
author: oli_rice,dthyresson
|
||||
image: launch-week-4/tuesday-graphql/graphql-thumb-og.png
|
||||
thumb: launch-week-4/tuesday-graphql/graphql-thumb.png
|
||||
@@ -11,10 +11,16 @@ date: '2022-03-29'
|
||||
toc_depth: 3
|
||||
---
|
||||
|
||||
GraphQL support is now in general availability on the Supabase platform via our [open source](https://github.com/supabase/pg_graphql/) PostgreSQL extension,
|
||||
[`pg_graphql](https://supabase.github.io/pg_graphql/) (beta)`.
|
||||
<div className="bg-gray-300 rounded-lg px-6 py-2 italic">
|
||||
|
||||

|
||||
🆕 `pg_graphql` has undergone significant enhancements since this announcement. Here is what is new:
|
||||
|
||||
- [pg_graphql v1.0](https://supabase.com/blog/pg-graphql-v1)
|
||||
- [New Features in pg_graphql v1.2](https://supabase.com/blog/whats-new-in-pg-graphql-v1-2)
|
||||
|
||||
</div>
|
||||
|
||||
GraphQL support is now in general availability on the Supabase platform via our [open source](https://github.com/supabase/pg_graphql/) PostgreSQL extension, [`pg_graphql`](https://supabase.github.io/pg_graphql/).
|
||||
|
||||
`pg_graphql` enables you to query existing PostgreSQL databases using GraphQL, either from within SQL or over HTTP:
|
||||
|
||||
@@ -119,8 +125,7 @@ An advantage to embedding GraphQL directly in the database is that we can lean o
|
||||
|
||||
### Authentication
|
||||
|
||||
The GraphQL types exposed by `pg_graphql` are filtered according to the SQL role's [INSERT/UPDATE/DELETE permissions](https://www.postgresql.org/docs/current/sql-grant.html).
|
||||
At Supabase, each API request is resolved in the database using the role in the request's JWT.
|
||||
The GraphQL types exposed by `pg_graphql` are filtered according to the SQL role's [INSERT/UPDATE/DELETE permissions](https://www.postgresql.org/docs/current/sql-grant.html). At Supabase, each API request is resolved in the database using the role in the request's JWT.
|
||||
|
||||
Anonymous users receive the `anon` role, and logged in users get the `authenticated` role. In either case, pg_graphql resolves requests according to the SQL permissions.
|
||||
The [introspection schema](https://graphql.org/learn/introspection/) is similarly filtered to limit exposed types and fields to those that the user has permission to access.
|
||||
@@ -134,6 +139,7 @@ Another nice side effect of making PostgreSQL do the heavy lifting is that Graph
|
||||
## Performance
|
||||
|
||||
Each [free tier database](https://supabase.com/pricing) on the Supabase platform runs on a dedicated AWS t4g.micro instance with 2 vCPUs and 1 GB of memory.
|
||||
|
||||
To squeeze the most out of that limited hardware we had to make a few significant optimizations:
|
||||
|
||||
**GraphQL queries are always transpiled into exactly one SQL query**
|
||||
@@ -195,9 +201,11 @@ Which has no duplication of data.
|
||||
|
||||
The difference in payload size is negligible in this case, but as the number of 1-to-many joins grows, data duplication in the SQL response grows geometrically.
|
||||
|
||||
**Queries are cached as [prepared statements](https://www.postgresql.org/docs/current/sql-prepare.html)**
|
||||

|
||||
|
||||
After a GraphQL query is transpiled to SQL, it is added to the prepared statement cache so subsequent requests with the same structure (think pagination) can skip the transpilation step.
|
||||
**Queries are cached as prepared statements**
|
||||
|
||||
After a GraphQL query is transpiled to SQL, it is added to the [prepared statement](https://www.postgresql.org/docs/current/sql-prepare.html) cache so subsequent requests with the same structure (think pagination) can skip the transpilation step.
|
||||
|
||||
Using prepared statements also allows PostgreSQL to skip the overhead of computing a query plan. For small, on-index, queries,
|
||||
the query planning step can take several times as long as the query's execution time, so the saving is significant at scale.
|
||||
@@ -221,7 +229,7 @@ As a result of these optimizations, the throughput of a “hello world” equiva
|
||||
|
||||
</aside>
|
||||
|
||||
To enable GraphQL in your Supabase instance, enable `pg_graphql` from the dashboard.
|
||||
To enable GraphQL in your Supabase instance, enable `pg_graphql` from the [dashboard](https://app.supabase.com/).
|
||||
|
||||
<video width="99%" autoPlay="autoplay" muted playsInline controls={true}>
|
||||
<source
|
||||
@@ -319,3 +327,10 @@ Our first general availability release of `pg_graphql` supports:
|
||||
In the near term, we plan to fully support array and json/b types. Longer term, we intend to support views and custom mutations from user defined functions.
|
||||
|
||||
Didn't see the feature you're interested in? [Let us know](https://github.com/supabase/pg_graphql/issues)
|
||||
|
||||
## More pg_graphql
|
||||
|
||||
- [Introducing pg_graphql: A GraphQL extension for PostgreSQL](https://supabase.com/blog/pg-graphql)
|
||||
- [pg_graphql v1.0](https://supabase.com/blog/pg-graphql-v1)
|
||||
- [pg_graphql v1.2](https://supabase.com/blog/whats-new-in-pg-graphql-v1-2)
|
||||
|
||||
@@ -84,24 +84,24 @@ Time per request: 1.549 [ms] (mean, across all concurrent requests)
|
||||
|
||||
## Roadmap
|
||||
|
||||
<div className="bg-gray-300 rounded-lg px-6 py-2 italic">
|
||||
|
||||
🆕 Support for views, filtering options, and other new features were released with [pg_graphql v1.2](https://supabase.com/blog/whats-new-in-pg-graphql-v1-2)
|
||||
|
||||
</div>
|
||||
|
||||
While v1.0 is an important milestone for stability, there's plenty of room to expand the feature set. For example, some commonly requested features on the immediate roadmap are:
|
||||
|
||||
- Extended filtering options
|
||||
- `startsWith` for the `String` type
|
||||
- nestable `and`/ `or` blocks
|
||||
- Support for user-defined functions
|
||||
- Support for views
|
||||
- Support for views
|
||||
|
||||
Longer term we look forward to experimenting with more ambitions features like an API for migrations, integrations with 3rd-party services through [supabase/wrappers](https://github.com/supabase/wrappers), and a scalable solution for subscriptions.
|
||||
|
||||
## More Launch Week 6
|
||||
## More pg_graphql
|
||||
|
||||
- [Day 1: New Supabase Docs, built with Next.js](https://supabase.com/blog/new-supabase-docs-built-with-nextjs)
|
||||
- [Day 2: Supabase Storage v2: Image resizing and Smart CDN](https://supabase.com/blog/storage-image-resizing-smart-cdn)
|
||||
- [Day 3: Multi-factor Authentication via Row Level Security Enforcement](https://supabase.com/blog/mfa-auth-via-rls)
|
||||
- [Day 4: Supabase Wrappers, a Postgres FDW framework written in Rust](https://supabase.com/blog/postgres-foreign-data-wrappers-rust)
|
||||
- [Day 5: Supabase Vault is now in Beta](https://supabase.com/blog/vault-now-in-beta)
|
||||
- [Community Day](https://supabase.com/blog/launch-week-6-community-day)
|
||||
- [Point in Time Recovery is now available](https://supabase.com/blog/postgres-point-in-time-recovery)
|
||||
- [Custom Domain Names are now available](https://supabase.com/blog/custom-domain-names)
|
||||
- [Wrap Up: everything we shipped](https://supabase.com/blog/launch-week-6-wrap-up)
|
||||
- [Introducing pg_graphql: A GraphQL extension for PostgreSQL](https://supabase.com/blog/pg-graphql)
|
||||
- [GraphQL is now available in Supabase](https://supabase.com/blog/graphql-now-available)
|
||||
- [pg_graphql v1.0](https://supabase.com/blog/pg-graphql-v1)
|
||||
@@ -0,0 +1,203 @@
|
||||
---
|
||||
title: 'What''s New in pg_graphql v1.2'
|
||||
description: 'New Features in the v1.2 release of pg_graphql'
|
||||
launchweek: 7
|
||||
tags:
|
||||
- postgres
|
||||
- graphql
|
||||
- planetpg
|
||||
date: '2023-04-21'
|
||||
published_at: '2023-04-21:00:00.000-07:00'
|
||||
toc_depth: 3
|
||||
author: oli_rice
|
||||
image: launch-week-6/pggraphql/og-pg-graphql.png
|
||||
thumb: launch-week-6/pggraphql/og-pg-graphql.png
|
||||
---
|
||||
|
||||
It's been 4 months since the [1.0.0 release of pg_graphql](https://supabase.com/blog/pg-graphql-v1).
|
||||
Since then, we’ve pushed several features to improve the APIs that `pg_graphql` produces.
|
||||
|
||||
In this article, we’ll walk through those features and show examples of each.
|
||||
|
||||
### View Support
|
||||
|
||||
Prior to `v1.1`, `pg_graphql` would only reflect standard tables. Since then, views, materialized views, and foreign tables are now also reflected in the GraphQL schema.
|
||||
|
||||
For example:
|
||||
|
||||
```sql
|
||||
create view "ProjectOwner" as
|
||||
select
|
||||
acc.id,
|
||||
acc.name
|
||||
from
|
||||
account acc
|
||||
join role r
|
||||
on r.id = acc.role_id
|
||||
where
|
||||
acc.role = 'project_owner'
|
||||
```
|
||||
|
||||
Since all entities exposed by `pg_graphql` require primary keys, we must define that constraint for the view. We do that using a comment directive:
|
||||
|
||||
```sql
|
||||
comment on view "ProjectOwner"
|
||||
is '@graphql({"primary_key_columns": ["id"]})';
|
||||
```
|
||||
|
||||
Which yields the GraphQL type:
|
||||
|
||||
```graphql
|
||||
type ProjectOwner implements Node {
|
||||
nodeId: ID!
|
||||
id: UUID!
|
||||
name: String
|
||||
}
|
||||
```
|
||||
|
||||
With associated `Edge` and `Connection` types. That enables querying via:
|
||||
|
||||
```graphql
|
||||
{
|
||||
projectOwnerCollection (first: 2) {
|
||||
edges {
|
||||
node {
|
||||
nodeId
|
||||
name
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Additionally, simple views automatically support mutation events like inserts and updates. You might use these to migrate underlying tables while maintaining backwards compatibility with previous API versions.
|
||||
|
||||
## Filtering
|
||||
|
||||
Filtering in SQL is endlessly flexible. We’ve taken two incremental steps to bring more of that flexibility to the GraphQL interface.
|
||||
|
||||
### `is null` and `is not null`
|
||||
|
||||
Handling `null` values can be tricky in both SQL and GraphQL. However, there are similarities we can take advantage of.
|
||||
In `pg_graphql`, every scalar data type has its own filter type, such as `IntFilter` and `StringFilter`.
|
||||
Each of these filter types now includes an `is` argument, which allows you to filter based on whether a value is null or not null.
|
||||
You can do this by using `{is: NULL}` for `null` values and `{is: NOT_NULL}` for non-`null` values.
|
||||
|
||||
```graphql
|
||||
enum FilterIs {
|
||||
NULL
|
||||
NOT_NULL
|
||||
}
|
||||
|
||||
type IntFilter {
|
||||
...
|
||||
is: FilterIs
|
||||
}
|
||||
```
|
||||
|
||||
For example:
|
||||
|
||||
```graphql
|
||||
{
|
||||
blogCollection(filter: { name: {is: NULL}}) {
|
||||
...
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
to return all `blog`s where the `name` is `null`.
|
||||
|
||||
### **`like`**, **`ilike`**, and **`startsWith`**
|
||||
|
||||
Text filtering options in `pg_graphql` have historically been restricted to equality checks. The hesitation was due to concerns about exposing a default filter that is difficult to index. The combination of [citext](https://www.postgresql.org/docs/current/citext.html) and [PGroonga available on the platform](https://supabase.com/docs/guides/database/extensions/pgroonga) solves those scalability risks and enabled us to expand the `StringFilter` with options for `like` `ilike` and `startsWith`.
|
||||
|
||||
```graphql
|
||||
input StringFilter {
|
||||
eq: String
|
||||
...
|
||||
startsWith: String
|
||||
like: String
|
||||
ilike: String
|
||||
}
|
||||
```
|
||||
|
||||
Note that `startsWith` filters should be preferred where appropriate because they can leverage simple B-Tree indexes to improve performance.
|
||||
|
||||
```graphql
|
||||
{
|
||||
generalLedgerCollection(filter: {identifierCode: {startsWith: "BX1:"}}) {
|
||||
edges {
|
||||
node {
|
||||
nodeId
|
||||
identifierCode
|
||||
amount
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### GraphQL directives `@skip` and `@include`
|
||||
|
||||
The GraphQL spec has evolved over time. Although the spec is clear, it is common for GraphQL servers to selectively omit some chunks of functionality. For example, some frameworks intentionally do not expose an introspection schema as a form of security through obscurity.
|
||||
|
||||
`pg_graphql` aims to be unopinionated and adhere exactly to the spec. The `@skip` and `@include` directives are part of the [GraphQL core specification](https://spec.graphql.org/October2021/#sec--skip) and are now functional.
|
||||
|
||||
The **`@skip`** directive in GraphQL is used to conditionally skip a field or fragment during query execution based on a Boolean variable. It can be used to make the query more efficient by reducing the amount of data retrieved from the server.
|
||||
|
||||
The **`@include`** directive is the mirror of **`@skip`** where a field or fragment is conditionally included depending on the value of a Boolean variable.
|
||||
|
||||
Here's an example of how the **`@skip`** directive can be used in a GraphQL query:
|
||||
|
||||
```graphql
|
||||
query getBooks($includeDetails: Boolean!) {
|
||||
booksCollection {
|
||||
edges {
|
||||
node {
|
||||
id
|
||||
title
|
||||
description @skip(if: $includeDetails)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### User Defined Descriptions
|
||||
|
||||
Users can now use the [comment directive system to assign descriptions](https://supabase.github.io/pg_graphql/configuration/#description) to tables, views and columns.
|
||||
|
||||
```sql
|
||||
create table public.book(
|
||||
id int primary key,
|
||||
title text not null
|
||||
);
|
||||
|
||||
comment on table public.book
|
||||
is e'@graphql({"description": "a library book"})';
|
||||
|
||||
comment on column public.book.title
|
||||
is e'@graphql({"description": "the title of the book"})';
|
||||
```
|
||||
|
||||
GraphQL IDEs, such as GraphiQL render those descriptions, allowing developers to provide clearer API documentation.
|
||||
|
||||
<Img
|
||||
src="/images/blog/2023-04-19-pg_graphql-v1_2/description_directive.png"
|
||||
wide={false}
|
||||
alt="description comment directive"
|
||||
/>
|
||||
|
||||
## Roadmap
|
||||
|
||||
The headline features we aim to launch in coming releases of `pg_graphql` include:
|
||||
|
||||
1. Support for user-defined functions: [GitHub issue](https://github.com/supabase/pg_graphql/issues/222)
|
||||
2. Support for nested inserts: [GitHub issue](https://github.com/supabase/pg_graphql/issues/294)
|
||||
3. An alternative approach to computed relationships based on SQL functions returning **`SET OF`** rather than comment directives (compatible with PostgREST)
|
||||
|
||||
## More pg_graphql
|
||||
|
||||
- [Introducing pg_graphql: A GraphQL extension for PostgreSQL](https://supabase.com/blog/pg-graphql)
|
||||
- [GraphQL is now available in Supabase](https://supabase.com/blog/graphql-now-available)
|
||||
- [pg_graphql v1.0](https://supabase.com/blog/pg-graphql-v1)
|
||||
@@ -0,0 +1,124 @@
|
||||
---
|
||||
title: 'Launch Week 7 Hackathon Winners'
|
||||
description: Announcing the winners of the Launch Week 7 Hackathon!
|
||||
author: tyler_shukert
|
||||
image: lw7-hackathon-winners/lw7-hackathon-winner.jpg
|
||||
thumb: lw7-hackathon-winners/lw7-hackathon-winner.jpg
|
||||
tags:
|
||||
- hackathon
|
||||
date: '2023-04-24'
|
||||
toc_depth: 2
|
||||
---
|
||||
|
||||
[Launch Week 7](https://supabase.com/launch-week) passed by, but we still have our favorite part left, celebrating the winners of the hackathon! We added an AI theme to our hackathon, and the results were exceptional. The quality of the apps was out of this world.
|
||||
|
||||
And now, without further ado, congratulations to the winners of Supabase Launch Week 7 Hackathon!
|
||||
|
||||
## Best Overall Project
|
||||
|
||||
### Winner
|
||||
|
||||
[Page Assist](https://github.com/n4ze3m/page-assist) - by [@n4ze3m](https://twitter.com/n4ze3m)
|
||||
|
||||
Revolutionize your browsing experience with PageAssist, an open-source Chrome extension that allows you to easily chat with any webpage using the power of ChatGPT API.
|
||||
|
||||

|
||||
|
||||
### Runner Up
|
||||
|
||||
[rect](https://github.com/Vandivier/rect) - by [@JohnVandivier](https://twitter.com/JohnVandivier)
|
||||
|
||||
Rect takes short-form social media content as an input, and produces four kinds of educational output material. Specifically, Rect is an AI-powered assistant that will produce flash cards, quizzes, slideshows, and a chatbot assistant.
|
||||
|
||||

|
||||
|
||||
## Best use of AI
|
||||
|
||||
### Winner
|
||||
|
||||
[Supathreads](https://github.com/asrvd/supathreads) - by [@_asheeshh](https://twitter.com/_asheeshh)
|
||||
|
||||
Supathreads creates concise and professional Twitter threads from your blog posts within seconds using the power of AI.
|
||||
|
||||

|
||||
|
||||
### Runner Up
|
||||
|
||||
[ChatVox · Chat With Any Video](https://github.com/JimmyLv/ChatVox) - by [@Jimmy_JingLv](https://twitter.com/Jimmy_JingLv)
|
||||
|
||||
ChatVox takes the YouTube/Podcast link you enter or the local Video/Audio file (WIP) you upload on the page, then you can chat with the video to obtain information about it.
|
||||
|
||||

|
||||
|
||||
## Most fun / best easter egg
|
||||
|
||||
### Winner
|
||||
|
||||
[Replay.ai - spotlight search for your browsing history](https://github.com/KamaniBhavin/replay) - by [@bhavinkamani_](https://twitter.com/bhavinkamani_)
|
||||
|
||||
Replay is a Chrome extension that lets you search through all the pages in your browsing history to find the one you can't remember. It uses cosine similarity search to find the page based on the content of the page and the search query provided by the user.
|
||||
|
||||

|
||||
|
||||
### Runner Up
|
||||
|
||||
[Groove AI - AI Generated Drum Patterns](https://github.com/danryland/groove-ai) - by [@RealDanRyland](https://twitter.com/RealDanRyland)
|
||||
|
||||
Groove AI generates drum patterns powered by AI and randomly plays the pattern whenever you open the page. You can modify the pattern by toggling each beat.
|
||||
|
||||

|
||||
|
||||
## Most technically impressive
|
||||
|
||||
### Winner
|
||||
|
||||
[Supa0SQL](https://github.com/Gabrielpanga/supa0sql) - by [@gpangantes](https://twitter.com/gpangantes)
|
||||
|
||||
Supa0SQL is a simple, easy-to-use, and fast Natural Language to Chart tool. It's meant for non-technical users to be able to quickly and easily create charts from their data without having to learn SQL.
|
||||
|
||||

|
||||
|
||||
### Runner Up
|
||||
|
||||
[Schedurio - An open-source tweet Scheduler.](https://github.com/Ansh-Rathod/schedurio) - by [@anshrathodfr](https://twitter.com/anshrathodfr)
|
||||
|
||||
Schedurio is very helpful for Twitter content creators who are paying monthly for services like Hypefury, Feedhive, and many others, now they can use the Schedurio open-source app on their desktop by adding their own API keys. currently available for the Macos and web.
|
||||
|
||||

|
||||
|
||||
|
||||
## Most visually pleasing
|
||||
|
||||
### Winner
|
||||
|
||||
[Generation: Hotdog](https://github.com/laznic/generation-hotdog) - by [@laznic](https://twitter.com/laznic)
|
||||
|
||||
Generation: Hotdog generates hotdog images from 5 emojis. It uses both OpenAI and Stable Diffusion APIs to generate the end results. First, it'll use the selected emojis to generate a description of a hotdog via OpenAI, and then it'll also translate that text to Japanese Kanji (also via OpenAI).
|
||||
|
||||

|
||||
|
||||
### Runner Up
|
||||
|
||||
[Tune Twisters](https://github.com/JohnPhamous/tune-twisters) - by [@johnphamous](https://twitter.com/johnphamous)
|
||||
|
||||
Tune Twisters is a simple game where it plays a random song in reverse and you have to guess the name of the song.
|
||||
|
||||

|
||||
|
||||
## The Prizes
|
||||
|
||||
As promised in the [hackathon announcement article](https://supabase.com/blog/launch-week-7-hackathon) the winners of the Best overall project will be granted the extremely limited edition Supabase Launch Week 7 Keyboard!
|
||||
|
||||

|
||||
|
||||
Each member of the winners and runner-ups in other categories will receive a limited edition Launch Week 7 swag kit.
|
||||
|
||||

|
||||
|
||||
### Get Started Guides
|
||||
|
||||
If you're inspired to build, check out some of the latest resources:
|
||||
|
||||
- **[Quick Start Guides](https://supabase.com/docs/guides/getting-started)**
|
||||
- **[Supabase Storage Guide](https://supabase.com/docs/guides/storage)**
|
||||
- **[Edge Functions Guide](https://supabase.com/docs/guides/functions)**
|
||||
@@ -2,7 +2,7 @@ import React, { useState } from 'react'
|
||||
import Link from 'next/link'
|
||||
import { useRouter } from 'next/router'
|
||||
|
||||
import { Button, Badge, IconStar, IconChevronDown } from 'ui'
|
||||
import { Button, Badge, Announcement, AnnouncementCountdown, IconStar, IconChevronDown } from 'ui'
|
||||
import FlyOut from '~/components/UI/FlyOut'
|
||||
import Transition from 'lib/Transition'
|
||||
|
||||
@@ -10,8 +10,7 @@ import SolutionsData from 'data/Solutions.json'
|
||||
|
||||
import Solutions from '~/components/Nav/Product'
|
||||
import Developers from '~/components/Nav/Developers'
|
||||
import Announcement from '~/components/Nav/Announcement'
|
||||
import CountdownBanner from '~/components/LaunchWeek/Banners/CountdownBanner'
|
||||
|
||||
import ScrollProgress from '~/components/ScrollProgress'
|
||||
|
||||
import { useIsLoggedIn, useTheme } from 'common'
|
||||
@@ -19,6 +18,7 @@ import TextLink from '../TextLink'
|
||||
import Image from 'next/image'
|
||||
import * as supabaseLogoWordmarkDark from 'common/assets/images/supabase-logo-wordmark--dark.png'
|
||||
import * as supabaseLogoWordmarkLight from 'common/assets/images/supabase-logo-wordmark--light.png'
|
||||
|
||||
import * as supabaseLogoWordmarkWhite from 'common/assets/images/supabase-logo-wordmark--white.png'
|
||||
|
||||
const Nav = () => {
|
||||
@@ -198,7 +198,7 @@ const Nav = () => {
|
||||
return (
|
||||
<>
|
||||
<Announcement>
|
||||
<CountdownBanner />
|
||||
<AnnouncementCountdown />
|
||||
</Announcement>
|
||||
<div className="sticky top-0 z-40 transform" style={{ transform: 'translate3d(0,0,999px)' }}>
|
||||
<div
|
||||
|
||||
@@ -1908,4 +1908,9 @@ module.exports = [
|
||||
source: '/docs/support',
|
||||
destination: '/support',
|
||||
},
|
||||
{
|
||||
permanent: true,
|
||||
source: '/docs/guides/cli/local-development',
|
||||
destination: '/docs/guides/getting-started/local-development',
|
||||
},
|
||||
]
|
||||
|
After Width: | Height: | Size: 90 KiB |
|
After Width: | Height: | Size: 651 KiB |
|
After Width: | Height: | Size: 512 KiB |
|
After Width: | Height: | Size: 431 KiB |
|
After Width: | Height: | Size: 491 KiB |
|
After Width: | Height: | Size: 401 KiB |
|
After Width: | Height: | Size: 520 KiB |
|
After Width: | Height: | Size: 347 KiB |
|
After Width: | Height: | Size: 742 KiB |
|
After Width: | Height: | Size: 350 KiB |
|
After Width: | Height: | Size: 1.0 MiB |
|
After Width: | Height: | Size: 869 KiB |
|
After Width: | Height: | Size: 93 KiB |
|
After Width: | Height: | Size: 710 KiB |
@@ -0,0 +1,39 @@
|
||||
import React from 'https://esm.sh/react@18.2.0?deno-std=0.140.0'
|
||||
import type { FormattedTweet } from './types.ts'
|
||||
|
||||
/**
|
||||
* Supports plain text, images, quote tweets.
|
||||
*
|
||||
* Needs support for images, GIFs, and replies maybe?
|
||||
* Styles use !important to override Tailwind .prose inside MDX.
|
||||
*/
|
||||
export default function Tweet({
|
||||
id,
|
||||
text,
|
||||
author,
|
||||
media,
|
||||
created_at,
|
||||
public_metrics,
|
||||
referenced_tweets,
|
||||
}: FormattedTweet) {
|
||||
const authorUrl = `https://twitter.com/${author.username}`
|
||||
const likeUrl = `https://twitter.com/intent/like?tweet_id=${id}`
|
||||
const retweetUrl = `https://twitter.com/intent/retweet?tweet_id=${id}`
|
||||
const replyUrl = `https://twitter.com/intent/tweet?in_reply_to=${id}`
|
||||
const tweetUrl = `https://twitter.com/${author.username}/status/${id}`
|
||||
const createdAt = new Date(created_at)
|
||||
|
||||
const formattedText = text.replace(/https:\/\/[\n\S]+/g, '').replace('&', '&')
|
||||
const quoteTweet = referenced_tweets && referenced_tweets.find((t) => t.type === 'quoted')
|
||||
|
||||
return (
|
||||
<div tw="flex">
|
||||
<div tw="flex flex-col md:flex-row w-full py-12 px-4 md:items-center justify-between p-8">
|
||||
<h2 tw="flex flex-col text-3xl sm:text-4xl font-bold tracking-tight text-left">
|
||||
<span>{formattedText}</span>
|
||||
<span tw="text-gray-300">{`- @${author.username}`}</span>
|
||||
</h2>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
import type { TwitterApiResponse, Tweet, FormattedTweet } from './types.ts'
|
||||
|
||||
export const getTweets: (ids: string[]) => Promise<FormattedTweet[]> = async (ids: string[]) => {
|
||||
if (ids.length === 0) {
|
||||
return []
|
||||
}
|
||||
|
||||
const queryParams = new URLSearchParams({
|
||||
ids: ids.join(','),
|
||||
expansions:
|
||||
'author_id,attachments.media_keys,referenced_tweets.id,referenced_tweets.id.author_id',
|
||||
'tweet.fields':
|
||||
'attachments,author_id,public_metrics,created_at,id,in_reply_to_user_id,referenced_tweets,text',
|
||||
'user.fields': 'id,name,profile_image_url,protected,url,username,verified',
|
||||
'media.fields': 'duration_ms,height,media_key,preview_image_url,type,url,width,public_metrics',
|
||||
})
|
||||
|
||||
const response = await fetch(`https://api.twitter.com/2/tweets?${queryParams}`, {
|
||||
headers: {
|
||||
Authorization: `Bearer ${Deno.env.get('TWITTER_API_TOKEN')}`,
|
||||
},
|
||||
})
|
||||
|
||||
const tweets: TwitterApiResponse = await response.json()
|
||||
|
||||
const getAuthorInfo = (author_id: string) => {
|
||||
return tweets.includes.users.find((user) => user.id === author_id)
|
||||
}
|
||||
|
||||
const getReferencedTweets = (mainTweet: Tweet) => {
|
||||
return (
|
||||
mainTweet?.referenced_tweets?.map((referencedTweet) => {
|
||||
const fullReferencedTweet = tweets.includes.tweets.find(
|
||||
(tweet) => tweet.id === referencedTweet.id
|
||||
)
|
||||
|
||||
return {
|
||||
type: referencedTweet.type,
|
||||
author: getAuthorInfo(fullReferencedTweet!.author_id),
|
||||
...fullReferencedTweet,
|
||||
}
|
||||
}) || []
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
tweets.data.reduce<any>((allTweets, tweet) => {
|
||||
const tweetWithAuthor = {
|
||||
...tweet,
|
||||
media:
|
||||
tweet?.attachments?.media_keys.map((key: string) =>
|
||||
tweets.includes.media.find((media) => media.media_key === key)
|
||||
) || [],
|
||||
referenced_tweets: getReferencedTweets(tweet),
|
||||
author: getAuthorInfo(tweet.author_id),
|
||||
}
|
||||
|
||||
return [tweetWithAuthor, ...allTweets]
|
||||
}, []) || [] // If the Twitter API key isn't set, don't break the build
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
import React from 'https://esm.sh/react@18.2.0?deno-std=0.140.0'
|
||||
import { ImageResponse } from 'https://deno.land/x/og_edge@0.0.4/mod.ts'
|
||||
import { createClient } from 'https://esm.sh/@supabase/supabase-js@2.5.0'
|
||||
import { corsHeaders } from '../_shared/cors.ts'
|
||||
import { getTweets } from './getTweet.ts'
|
||||
import Tweet from './Tweet.tsx'
|
||||
|
||||
const STORAGE_URL =
|
||||
'https://obuldanrptloktxcffvn.supabase.co/storage/v1/object/public/images/tweet-to-image'
|
||||
|
||||
// Load custom font
|
||||
const FONT_URL = `${STORAGE_URL}/CircularStd-Book.otf`
|
||||
const font = fetch(new URL(FONT_URL, import.meta.url)).then((res) => res.arrayBuffer())
|
||||
|
||||
export async function handler(req: Request) {
|
||||
const url = new URL(req.url)
|
||||
const tweetId = url.searchParams.get('tweetId')
|
||||
|
||||
if (!tweetId)
|
||||
return new Response(JSON.stringify({ error: 'missing tweetId param' }), {
|
||||
headers: { ...corsHeaders, 'Content-Type': 'application/json' },
|
||||
status: 400,
|
||||
})
|
||||
|
||||
try {
|
||||
// Try to get image from Supabase Storage CDN.
|
||||
const storageResponse = await fetch(`${STORAGE_URL}/${tweetId}.png`)
|
||||
if (storageResponse.ok) return storageResponse
|
||||
|
||||
// Else, generate image and upload to storage.
|
||||
const fontData = await font
|
||||
|
||||
const tweets = await getTweets([tweetId])
|
||||
const tweet = tweets[0]
|
||||
console.log('formattedTweets', JSON.stringify(tweets, null, 2))
|
||||
const generatedImage = new ImageResponse(
|
||||
(
|
||||
<div
|
||||
style={{
|
||||
height: '100%',
|
||||
width: '100%',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
backgroundColor: '#1c1c1c',
|
||||
color: '#EDEDED',
|
||||
}}
|
||||
>
|
||||
<Tweet {...tweet} />
|
||||
</div>
|
||||
),
|
||||
{
|
||||
width: 1200,
|
||||
height: 630,
|
||||
// Supported options: 'twemoji', 'blobmoji', 'noto', 'openmoji', 'fluent', 'fluentFlat'
|
||||
// Default to 'twemoji'
|
||||
emoji: 'twemoji',
|
||||
fonts: [
|
||||
{
|
||||
name: 'Circular',
|
||||
data: fontData,
|
||||
style: 'normal',
|
||||
},
|
||||
],
|
||||
}
|
||||
)
|
||||
|
||||
const supabaseAdminClient = createClient(
|
||||
// Supabase API URL - env var exported by default when deployed.
|
||||
Deno.env.get('SUPABASE_URL') ?? '',
|
||||
// Supabase API SERVICE ROLE KEY - env var exported by default when deployed.
|
||||
Deno.env.get('SUPABASE_SERVICE_ROLE_KEY') ?? ''
|
||||
)
|
||||
|
||||
// Upload image to storage.
|
||||
const { error } = await supabaseAdminClient.storage
|
||||
.from('images')
|
||||
.upload(`tweet-to-image/${tweetId}.png`, generatedImage.body!, {
|
||||
contentType: 'image/png',
|
||||
cacheControl: '31536000',
|
||||
upsert: false,
|
||||
})
|
||||
if (error) throw error
|
||||
|
||||
return generatedImage
|
||||
} catch (error) {
|
||||
return new Response(JSON.stringify({ error: error.message }), {
|
||||
headers: { ...corsHeaders, 'Content-Type': 'application/json' },
|
||||
status: 400,
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
// Follow this setup guide to integrate the Deno language server with your editor:
|
||||
// https://deno.land/manual/getting_started/setup_your_environment
|
||||
// This enables autocomplete, go to definition, etc.
|
||||
|
||||
import { serve } from 'https://deno.land/std@0.140.0/http/server.ts'
|
||||
|
||||
import { handler } from './handler.tsx'
|
||||
|
||||
console.log(`Function "tweet-to-image" up and running!`)
|
||||
|
||||
serve(handler)
|
||||
@@ -0,0 +1,74 @@
|
||||
export type Tweet = {
|
||||
edit_history_tweet_ids: string[]
|
||||
text: string
|
||||
referenced_tweets:
|
||||
| Array<{
|
||||
type: string
|
||||
id: string
|
||||
}>
|
||||
| undefined
|
||||
attachments: {
|
||||
media_keys: string[]
|
||||
}
|
||||
id: string
|
||||
created_at: string
|
||||
public_metrics: {
|
||||
retweet_count: number
|
||||
reply_count: number
|
||||
like_count: number
|
||||
quote_count: number
|
||||
impression_count: number
|
||||
}
|
||||
author_id: string
|
||||
author?: User
|
||||
}
|
||||
|
||||
export type Media = {
|
||||
width: number
|
||||
media_key: string
|
||||
type: string
|
||||
url: string
|
||||
height: number
|
||||
}
|
||||
|
||||
export type User = {
|
||||
name: string
|
||||
url: string
|
||||
verified: boolean
|
||||
profile_image_url: string
|
||||
id: string
|
||||
username: string
|
||||
protected: boolean
|
||||
}
|
||||
|
||||
// raw
|
||||
export type TwitterApiResponse = {
|
||||
data: Tweet[]
|
||||
includes: {
|
||||
media: Media[]
|
||||
users: User[]
|
||||
tweets: Tweet[]
|
||||
}
|
||||
}
|
||||
|
||||
export type FormattedTweet = {
|
||||
type: string
|
||||
edit_history_tweet_ids: string[]
|
||||
text: string
|
||||
referenced_tweets: FormattedTweet[]
|
||||
attachments: {
|
||||
media_keys: string[]
|
||||
}
|
||||
id: string
|
||||
created_at: string
|
||||
public_metrics: {
|
||||
retweet_count: number
|
||||
reply_count: number
|
||||
like_count: number
|
||||
quote_count: number
|
||||
impression_count: number
|
||||
}
|
||||
author_id: string
|
||||
media: Media[]
|
||||
author: User
|
||||
}
|
||||
@@ -5952,6 +5952,25 @@
|
||||
"resolved": "https://registry.npmjs.org/@octokit/webhooks-types/-/webhooks-types-6.10.0.tgz",
|
||||
"integrity": "sha512-lDNv83BeEyxxukdQ0UttiUXawk9+6DkdjjFtm2GFED+24IQhTVaoSbwV9vWWKONyGLzRmCQqZmoEWkDhkEmPlw=="
|
||||
},
|
||||
"node_modules/@pkgr/utils": {
|
||||
"version": "2.3.1",
|
||||
"resolved": "https://registry.npmjs.org/@pkgr/utils/-/utils-2.3.1.tgz",
|
||||
"integrity": "sha512-wfzX8kc1PMyUILA+1Z/EqoE4UCXGy0iRGMhPwdfae1+f0OXlLqCk+By+aMzgJBzR9AzS4CDizioG6Ss1gvAFJw==",
|
||||
"dependencies": {
|
||||
"cross-spawn": "^7.0.3",
|
||||
"is-glob": "^4.0.3",
|
||||
"open": "^8.4.0",
|
||||
"picocolors": "^1.0.0",
|
||||
"tiny-glob": "^0.2.9",
|
||||
"tslib": "^2.4.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^12.20.0 || ^14.18.0 || >=16.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/unts"
|
||||
}
|
||||
},
|
||||
"node_modules/@pmmmwh/react-refresh-webpack-plugin": {
|
||||
"version": "0.5.10",
|
||||
"resolved": "https://registry.npmjs.org/@pmmmwh/react-refresh-webpack-plugin/-/react-refresh-webpack-plugin-0.5.10.tgz",
|
||||
@@ -15827,7 +15846,6 @@
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-2.0.0.tgz",
|
||||
"integrity": "sha512-Ds09qNh8yw3khSjiJjiUInaGX9xlqZDY7JVryGxdxV7NPeuqQfplOpQ66yJFZut3jLa5zOwkXw1g9EI2uKh4Og==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
@@ -19020,7 +19038,6 @@
|
||||
"version": "4.5.0",
|
||||
"resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.5.0.tgz",
|
||||
"integrity": "sha512-MjhiaIWCJ1sAU4pIQ5i5OfOuHHxVo1oYeNsWTON7jxYkod8pHocXeh+SSbmu5OZZZK73B6cbJ2XADzXehLyovQ==",
|
||||
"dev": true,
|
||||
"funding": {
|
||||
"url": "https://github.com/privatenumber/get-tsconfig?sponsor=1"
|
||||
}
|
||||
@@ -19127,6 +19144,11 @@
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/globalyzer": {
|
||||
"version": "0.1.0",
|
||||
"resolved": "https://registry.npmjs.org/globalyzer/-/globalyzer-0.1.0.tgz",
|
||||
"integrity": "sha512-40oNTM9UfG6aBmuKxk/giHn5nQ8RVz/SS4Ir6zgzOv9/qC3kKZ9v4etGTcJbEl/NyVQH7FGU7d+X1egr57Md2Q=="
|
||||
},
|
||||
"node_modules/globby": {
|
||||
"version": "12.2.0",
|
||||
"resolved": "https://registry.npmjs.org/globby/-/globby-12.2.0.tgz",
|
||||
@@ -19157,6 +19179,11 @@
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/globrex": {
|
||||
"version": "0.1.2",
|
||||
"resolved": "https://registry.npmjs.org/globrex/-/globrex-0.1.2.tgz",
|
||||
"integrity": "sha512-uHJgbwAMwNFf5mLst7IWLNg14x1CkeqglJb/K3doi4dw6q2IvAAmM/Y81kevy83wP+Sst+nutFTYOGg3d1lsxg=="
|
||||
},
|
||||
"node_modules/goober": {
|
||||
"version": "2.1.12",
|
||||
"resolved": "https://registry.npmjs.org/goober/-/goober-2.1.12.tgz",
|
||||
@@ -20885,7 +20912,6 @@
|
||||
"version": "2.2.1",
|
||||
"resolved": "https://registry.npmjs.org/is-docker/-/is-docker-2.2.1.tgz",
|
||||
"integrity": "sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==",
|
||||
"dev": true,
|
||||
"bin": {
|
||||
"is-docker": "cli.js"
|
||||
},
|
||||
@@ -21301,7 +21327,6 @@
|
||||
"version": "2.2.0",
|
||||
"resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz",
|
||||
"integrity": "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"is-docker": "^2.0.0"
|
||||
},
|
||||
@@ -28220,7 +28245,6 @@
|
||||
"version": "8.4.2",
|
||||
"resolved": "https://registry.npmjs.org/open/-/open-8.4.2.tgz",
|
||||
"integrity": "sha512-7x81NCL719oNbsq/3mh+hVrAWmFuEYUqrq/Iw3kUzH8ReypT9QQ0BLoJS7/G9k6N81XjW4qHWtjWwe/9eLy1EQ==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"define-lazy-prop": "^2.0.0",
|
||||
"is-docker": "^2.1.1",
|
||||
@@ -35342,6 +35366,21 @@
|
||||
"integrity": "sha512-AsS729u2RHUfEra9xJrE39peJcc2stq2+poBXX8bcM08Y6g9j/i/PUzwNQqkaJde7Ntg1TO7bSREbR5sdosQ+g==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/synckit": {
|
||||
"version": "0.8.5",
|
||||
"resolved": "https://registry.npmjs.org/synckit/-/synckit-0.8.5.tgz",
|
||||
"integrity": "sha512-L1dapNV6vu2s/4Sputv8xGsCdAVlb5nRDMFU/E27D44l5U6cw1g0dGd45uLc+OXjNMmF4ntiMdCimzcjFKQI8Q==",
|
||||
"dependencies": {
|
||||
"@pkgr/utils": "^2.3.1",
|
||||
"tslib": "^2.5.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^14.18.0 || >=16.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/unts"
|
||||
}
|
||||
},
|
||||
"node_modules/tabbable": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/tabbable/-/tabbable-4.0.0.tgz",
|
||||
@@ -35798,6 +35837,15 @@
|
||||
"resolved": "https://registry.npmjs.org/tiny-emitter/-/tiny-emitter-2.1.0.tgz",
|
||||
"integrity": "sha512-NB6Dk1A9xgQPMoGqC5CVXn123gWyte215ONT5Pp5a0yt4nlEoO1ZWeCwpncaekPHXO60i47ihFnZPiRPjRMq4Q=="
|
||||
},
|
||||
"node_modules/tiny-glob": {
|
||||
"version": "0.2.9",
|
||||
"resolved": "https://registry.npmjs.org/tiny-glob/-/tiny-glob-0.2.9.tgz",
|
||||
"integrity": "sha512-g/55ssRPUjShh+xkfx9UPDXqhckHEsHr4Vd9zX55oSdGZc/MD0m3sferOkwWtp98bv+kcVfEHtRJgBVJzelrzg==",
|
||||
"dependencies": {
|
||||
"globalyzer": "0.1.0",
|
||||
"globrex": "^0.1.2"
|
||||
}
|
||||
},
|
||||
"node_modules/tiny-invariant": {
|
||||
"version": "1.3.1",
|
||||
"resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.3.1.tgz",
|
||||
@@ -39108,6 +39156,7 @@
|
||||
"config": "*",
|
||||
"configcat-js": "^7.0.0",
|
||||
"dayjs": "^1.11.0",
|
||||
"eslint-config-next": "^13.3.0",
|
||||
"file-saver": "^2.0.5",
|
||||
"generate-password": "^1.7.0",
|
||||
"graphql": "^16.6.0",
|
||||
@@ -39122,7 +39171,6 @@
|
||||
"mobx": "^6.3.2",
|
||||
"mobx-react-lite": "^3.2.0",
|
||||
"next": "12.3.2",
|
||||
"next-compose-plugins": "^2.2.1",
|
||||
"next-transpile-modules": "^9.0.0",
|
||||
"openapi-types": "^9.1.0",
|
||||
"p-queue": "^6.6.2",
|
||||
@@ -39221,6 +39269,14 @@
|
||||
"webpack-bundle-analyzer": "4.3.0"
|
||||
}
|
||||
},
|
||||
"studio/node_modules/@next/eslint-plugin-next": {
|
||||
"version": "13.3.0",
|
||||
"resolved": "https://registry.npmjs.org/@next/eslint-plugin-next/-/eslint-plugin-next-13.3.0.tgz",
|
||||
"integrity": "sha512-wuGN5qSEjSgcq9fVkH0Y/qIPFjnZtW3ZPwfjJOn7l/rrf6y8J24h/lo61kwqunTyzZJm/ETGfGVU9PUs8cnzEA==",
|
||||
"dependencies": {
|
||||
"glob": "7.1.7"
|
||||
}
|
||||
},
|
||||
"studio/node_modules/@tailwindcss/typography": {
|
||||
"version": "0.5.8",
|
||||
"dev": true,
|
||||
@@ -39475,6 +39531,132 @@
|
||||
"d3-time": "1 - 2"
|
||||
}
|
||||
},
|
||||
"studio/node_modules/doctrine": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz",
|
||||
"integrity": "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==",
|
||||
"dependencies": {
|
||||
"esutils": "^2.0.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"studio/node_modules/eslint-config-next": {
|
||||
"version": "13.3.0",
|
||||
"resolved": "https://registry.npmjs.org/eslint-config-next/-/eslint-config-next-13.3.0.tgz",
|
||||
"integrity": "sha512-6YEwmFBX0VjBd3ODGW9df0Is0FLaRFdMN8eAahQG9CN6LjQ28J8AFr19ngxqMSg7Qv6Uca/3VeeBosJh1bzu0w==",
|
||||
"dependencies": {
|
||||
"@next/eslint-plugin-next": "13.3.0",
|
||||
"@rushstack/eslint-patch": "^1.1.3",
|
||||
"@typescript-eslint/parser": "^5.42.0",
|
||||
"eslint-import-resolver-node": "^0.3.6",
|
||||
"eslint-import-resolver-typescript": "^3.5.2",
|
||||
"eslint-plugin-import": "^2.26.0",
|
||||
"eslint-plugin-jsx-a11y": "^6.5.1",
|
||||
"eslint-plugin-react": "^7.31.7",
|
||||
"eslint-plugin-react-hooks": "^4.5.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"eslint": "^7.23.0 || ^8.0.0",
|
||||
"typescript": ">=3.3.1"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"typescript": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"studio/node_modules/eslint-import-resolver-typescript": {
|
||||
"version": "3.5.5",
|
||||
"resolved": "https://registry.npmjs.org/eslint-import-resolver-typescript/-/eslint-import-resolver-typescript-3.5.5.tgz",
|
||||
"integrity": "sha512-TdJqPHs2lW5J9Zpe17DZNQuDnox4xo2o+0tE7Pggain9Rbc19ik8kFtXdxZ250FVx2kF4vlt2RSf4qlUpG7bhw==",
|
||||
"dependencies": {
|
||||
"debug": "^4.3.4",
|
||||
"enhanced-resolve": "^5.12.0",
|
||||
"eslint-module-utils": "^2.7.4",
|
||||
"get-tsconfig": "^4.5.0",
|
||||
"globby": "^13.1.3",
|
||||
"is-core-module": "^2.11.0",
|
||||
"is-glob": "^4.0.3",
|
||||
"synckit": "^0.8.5"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^14.18.0 || >=16.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/unts/projects/eslint-import-resolver-ts"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"eslint": "*",
|
||||
"eslint-plugin-import": "*"
|
||||
}
|
||||
},
|
||||
"studio/node_modules/eslint-plugin-react": {
|
||||
"version": "7.32.2",
|
||||
"resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.32.2.tgz",
|
||||
"integrity": "sha512-t2fBMa+XzonrrNkyVirzKlvn5RXzzPwRHtMvLAtVZrt8oxgnTQaYbU6SXTOO1mwQgp1y5+toMSKInnzGr0Knqg==",
|
||||
"dependencies": {
|
||||
"array-includes": "^3.1.6",
|
||||
"array.prototype.flatmap": "^1.3.1",
|
||||
"array.prototype.tosorted": "^1.1.1",
|
||||
"doctrine": "^2.1.0",
|
||||
"estraverse": "^5.3.0",
|
||||
"jsx-ast-utils": "^2.4.1 || ^3.0.0",
|
||||
"minimatch": "^3.1.2",
|
||||
"object.entries": "^1.1.6",
|
||||
"object.fromentries": "^2.0.6",
|
||||
"object.hasown": "^1.1.2",
|
||||
"object.values": "^1.1.6",
|
||||
"prop-types": "^15.8.1",
|
||||
"resolve": "^2.0.0-next.4",
|
||||
"semver": "^6.3.0",
|
||||
"string.prototype.matchall": "^4.0.8"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=4"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"eslint": "^3 || ^4 || ^5 || ^6 || ^7 || ^8"
|
||||
}
|
||||
},
|
||||
"studio/node_modules/glob": {
|
||||
"version": "7.1.7",
|
||||
"resolved": "https://registry.npmjs.org/glob/-/glob-7.1.7.tgz",
|
||||
"integrity": "sha512-OvD9ENzPLbegENnYP5UUfJIirTg4+XwMWGaQfQTY0JenxNvvIKP3U3/tAQSPIu/lHxXYSZmpXlUHeqAIdKzBLQ==",
|
||||
"dependencies": {
|
||||
"fs.realpath": "^1.0.0",
|
||||
"inflight": "^1.0.4",
|
||||
"inherits": "2",
|
||||
"minimatch": "^3.0.4",
|
||||
"once": "^1.3.0",
|
||||
"path-is-absolute": "^1.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "*"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/isaacs"
|
||||
}
|
||||
},
|
||||
"studio/node_modules/globby": {
|
||||
"version": "13.1.4",
|
||||
"resolved": "https://registry.npmjs.org/globby/-/globby-13.1.4.tgz",
|
||||
"integrity": "sha512-iui/IiiW+QrJ1X1hKH5qwlMQyv34wJAYwH1vrf8b9kBA4sNiif3gKsMHa+BrdnOpEudWjpotfa7LrTzB1ERS/g==",
|
||||
"dependencies": {
|
||||
"dir-glob": "^3.0.1",
|
||||
"fast-glob": "^3.2.11",
|
||||
"ignore": "^5.2.0",
|
||||
"merge2": "^1.4.1",
|
||||
"slash": "^4.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^12.20.0 || ^14.13.1 || >=16.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"studio/node_modules/internmap": {
|
||||
"version": "1.0.1",
|
||||
"license": "ISC"
|
||||
@@ -39542,6 +39724,33 @@
|
||||
"react-dom": "^16.0.0 || ^17.0.0"
|
||||
}
|
||||
},
|
||||
"studio/node_modules/resolve": {
|
||||
"version": "2.0.0-next.4",
|
||||
"resolved": "https://registry.npmjs.org/resolve/-/resolve-2.0.0-next.4.tgz",
|
||||
"integrity": "sha512-iMDbmAWtfU+MHpxt/I5iWI7cY6YVEZUQ3MBgPQ++XD1PELuJHIl82xBmObyP2KyQmkNB2dsqF7seoQQiAn5yDQ==",
|
||||
"dependencies": {
|
||||
"is-core-module": "^2.9.0",
|
||||
"path-parse": "^1.0.7",
|
||||
"supports-preserve-symlinks-flag": "^1.0.0"
|
||||
},
|
||||
"bin": {
|
||||
"resolve": "bin/resolve"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"studio/node_modules/slash": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/slash/-/slash-4.0.0.tgz",
|
||||
"integrity": "sha512-3dOsAHXXUkQTpOYcoAxLIorMTp4gIQr5IW3iVb7A7lFIp0VHhnynm9izx6TssdrIcVIESAlVjtnO2K8bg+Coew==",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"studio/node_modules/sql-formatter": {
|
||||
"version": "12.2.0",
|
||||
"resolved": "https://registry.npmjs.org/sql-formatter/-/sql-formatter-12.2.0.tgz",
|
||||
@@ -39556,7 +39765,7 @@
|
||||
},
|
||||
"studio/node_modules/typescript": {
|
||||
"version": "4.8.4",
|
||||
"dev": true,
|
||||
"devOptional": true,
|
||||
"license": "Apache-2.0",
|
||||
"bin": {
|
||||
"tsc": "bin/tsc",
|
||||
@@ -43408,6 +43617,19 @@
|
||||
"resolved": "https://registry.npmjs.org/@octokit/webhooks-types/-/webhooks-types-6.10.0.tgz",
|
||||
"integrity": "sha512-lDNv83BeEyxxukdQ0UttiUXawk9+6DkdjjFtm2GFED+24IQhTVaoSbwV9vWWKONyGLzRmCQqZmoEWkDhkEmPlw=="
|
||||
},
|
||||
"@pkgr/utils": {
|
||||
"version": "2.3.1",
|
||||
"resolved": "https://registry.npmjs.org/@pkgr/utils/-/utils-2.3.1.tgz",
|
||||
"integrity": "sha512-wfzX8kc1PMyUILA+1Z/EqoE4UCXGy0iRGMhPwdfae1+f0OXlLqCk+By+aMzgJBzR9AzS4CDizioG6Ss1gvAFJw==",
|
||||
"requires": {
|
||||
"cross-spawn": "^7.0.3",
|
||||
"is-glob": "^4.0.3",
|
||||
"open": "^8.4.0",
|
||||
"picocolors": "^1.0.0",
|
||||
"tiny-glob": "^0.2.9",
|
||||
"tslib": "^2.4.0"
|
||||
}
|
||||
},
|
||||
"@pmmmwh/react-refresh-webpack-plugin": {
|
||||
"version": "0.5.10",
|
||||
"resolved": "https://registry.npmjs.org/@pmmmwh/react-refresh-webpack-plugin/-/react-refresh-webpack-plugin-0.5.10.tgz",
|
||||
@@ -51064,8 +51286,7 @@
|
||||
"define-lazy-prop": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-2.0.0.tgz",
|
||||
"integrity": "sha512-Ds09qNh8yw3khSjiJjiUInaGX9xlqZDY7JVryGxdxV7NPeuqQfplOpQ66yJFZut3jLa5zOwkXw1g9EI2uKh4Og==",
|
||||
"dev": true
|
||||
"integrity": "sha512-Ds09qNh8yw3khSjiJjiUInaGX9xlqZDY7JVryGxdxV7NPeuqQfplOpQ66yJFZut3jLa5zOwkXw1g9EI2uKh4Og=="
|
||||
},
|
||||
"define-properties": {
|
||||
"version": "1.2.0",
|
||||
@@ -53971,8 +54192,7 @@
|
||||
"get-tsconfig": {
|
||||
"version": "4.5.0",
|
||||
"resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.5.0.tgz",
|
||||
"integrity": "sha512-MjhiaIWCJ1sAU4pIQ5i5OfOuHHxVo1oYeNsWTON7jxYkod8pHocXeh+SSbmu5OZZZK73B6cbJ2XADzXehLyovQ==",
|
||||
"dev": true
|
||||
"integrity": "sha512-MjhiaIWCJ1sAU4pIQ5i5OfOuHHxVo1oYeNsWTON7jxYkod8pHocXeh+SSbmu5OZZZK73B6cbJ2XADzXehLyovQ=="
|
||||
},
|
||||
"get-value": {
|
||||
"version": "2.0.6",
|
||||
@@ -54046,6 +54266,11 @@
|
||||
"define-properties": "^1.1.3"
|
||||
}
|
||||
},
|
||||
"globalyzer": {
|
||||
"version": "0.1.0",
|
||||
"resolved": "https://registry.npmjs.org/globalyzer/-/globalyzer-0.1.0.tgz",
|
||||
"integrity": "sha512-40oNTM9UfG6aBmuKxk/giHn5nQ8RVz/SS4Ir6zgzOv9/qC3kKZ9v4etGTcJbEl/NyVQH7FGU7d+X1egr57Md2Q=="
|
||||
},
|
||||
"globby": {
|
||||
"version": "12.2.0",
|
||||
"resolved": "https://registry.npmjs.org/globby/-/globby-12.2.0.tgz",
|
||||
@@ -54066,6 +54291,11 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"globrex": {
|
||||
"version": "0.1.2",
|
||||
"resolved": "https://registry.npmjs.org/globrex/-/globrex-0.1.2.tgz",
|
||||
"integrity": "sha512-uHJgbwAMwNFf5mLst7IWLNg14x1CkeqglJb/K3doi4dw6q2IvAAmM/Y81kevy83wP+Sst+nutFTYOGg3d1lsxg=="
|
||||
},
|
||||
"goober": {
|
||||
"version": "2.1.12",
|
||||
"resolved": "https://registry.npmjs.org/goober/-/goober-2.1.12.tgz",
|
||||
@@ -55352,8 +55582,7 @@
|
||||
"is-docker": {
|
||||
"version": "2.2.1",
|
||||
"resolved": "https://registry.npmjs.org/is-docker/-/is-docker-2.2.1.tgz",
|
||||
"integrity": "sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==",
|
||||
"dev": true
|
||||
"integrity": "sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ=="
|
||||
},
|
||||
"is-dom": {
|
||||
"version": "1.1.0",
|
||||
@@ -55630,7 +55859,6 @@
|
||||
"version": "2.2.0",
|
||||
"resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz",
|
||||
"integrity": "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"is-docker": "^2.0.0"
|
||||
}
|
||||
@@ -60911,7 +61139,6 @@
|
||||
"version": "8.4.2",
|
||||
"resolved": "https://registry.npmjs.org/open/-/open-8.4.2.tgz",
|
||||
"integrity": "sha512-7x81NCL719oNbsq/3mh+hVrAWmFuEYUqrq/Iw3kUzH8ReypT9QQ0BLoJS7/G9k6N81XjW4qHWtjWwe/9eLy1EQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"define-lazy-prop": "^2.0.0",
|
||||
"is-docker": "^2.1.1",
|
||||
@@ -66146,6 +66373,7 @@
|
||||
"config": "*",
|
||||
"configcat-js": "^7.0.0",
|
||||
"dayjs": "^1.11.0",
|
||||
"eslint-config-next": "*",
|
||||
"file-saver": "^2.0.5",
|
||||
"generate-password": "^1.7.0",
|
||||
"graphql": "^16.6.0",
|
||||
@@ -66162,7 +66390,6 @@
|
||||
"mobx": "^6.3.2",
|
||||
"mobx-react-lite": "^3.2.0",
|
||||
"next": "12.3.2",
|
||||
"next-compose-plugins": "^2.2.1",
|
||||
"next-transpile-modules": "^9.0.0",
|
||||
"openapi-types": "^9.1.0",
|
||||
"p-queue": "^6.6.2",
|
||||
@@ -66217,6 +66444,14 @@
|
||||
"webpack-bundle-analyzer": "4.3.0"
|
||||
}
|
||||
},
|
||||
"@next/eslint-plugin-next": {
|
||||
"version": "13.3.0",
|
||||
"resolved": "https://registry.npmjs.org/@next/eslint-plugin-next/-/eslint-plugin-next-13.3.0.tgz",
|
||||
"integrity": "sha512-wuGN5qSEjSgcq9fVkH0Y/qIPFjnZtW3ZPwfjJOn7l/rrf6y8J24h/lo61kwqunTyzZJm/ETGfGVU9PUs8cnzEA==",
|
||||
"requires": {
|
||||
"glob": "7.1.7"
|
||||
}
|
||||
},
|
||||
"@tailwindcss/typography": {
|
||||
"version": "0.5.8",
|
||||
"dev": true,
|
||||
@@ -66378,6 +66613,92 @@
|
||||
"d3-time": "1 - 2"
|
||||
}
|
||||
},
|
||||
"doctrine": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz",
|
||||
"integrity": "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==",
|
||||
"requires": {
|
||||
"esutils": "^2.0.2"
|
||||
}
|
||||
},
|
||||
"eslint-config-next": {
|
||||
"version": "13.3.0",
|
||||
"resolved": "https://registry.npmjs.org/eslint-config-next/-/eslint-config-next-13.3.0.tgz",
|
||||
"integrity": "sha512-6YEwmFBX0VjBd3ODGW9df0Is0FLaRFdMN8eAahQG9CN6LjQ28J8AFr19ngxqMSg7Qv6Uca/3VeeBosJh1bzu0w==",
|
||||
"requires": {
|
||||
"@next/eslint-plugin-next": "13.3.0",
|
||||
"@rushstack/eslint-patch": "^1.1.3",
|
||||
"@typescript-eslint/parser": "^5.42.0",
|
||||
"eslint-import-resolver-node": "^0.3.6",
|
||||
"eslint-import-resolver-typescript": "^3.5.2",
|
||||
"eslint-plugin-import": "^2.26.0",
|
||||
"eslint-plugin-jsx-a11y": "^6.5.1",
|
||||
"eslint-plugin-react": "^7.31.7",
|
||||
"eslint-plugin-react-hooks": "^4.5.0"
|
||||
}
|
||||
},
|
||||
"eslint-import-resolver-typescript": {
|
||||
"version": "3.5.5",
|
||||
"resolved": "https://registry.npmjs.org/eslint-import-resolver-typescript/-/eslint-import-resolver-typescript-3.5.5.tgz",
|
||||
"integrity": "sha512-TdJqPHs2lW5J9Zpe17DZNQuDnox4xo2o+0tE7Pggain9Rbc19ik8kFtXdxZ250FVx2kF4vlt2RSf4qlUpG7bhw==",
|
||||
"requires": {
|
||||
"debug": "^4.3.4",
|
||||
"enhanced-resolve": "^5.12.0",
|
||||
"eslint-module-utils": "^2.7.4",
|
||||
"get-tsconfig": "^4.5.0",
|
||||
"globby": "^13.1.3",
|
||||
"is-core-module": "^2.11.0",
|
||||
"is-glob": "^4.0.3",
|
||||
"synckit": "^0.8.5"
|
||||
}
|
||||
},
|
||||
"eslint-plugin-react": {
|
||||
"version": "7.32.2",
|
||||
"resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.32.2.tgz",
|
||||
"integrity": "sha512-t2fBMa+XzonrrNkyVirzKlvn5RXzzPwRHtMvLAtVZrt8oxgnTQaYbU6SXTOO1mwQgp1y5+toMSKInnzGr0Knqg==",
|
||||
"requires": {
|
||||
"array-includes": "^3.1.6",
|
||||
"array.prototype.flatmap": "^1.3.1",
|
||||
"array.prototype.tosorted": "^1.1.1",
|
||||
"doctrine": "^2.1.0",
|
||||
"estraverse": "^5.3.0",
|
||||
"jsx-ast-utils": "^2.4.1 || ^3.0.0",
|
||||
"minimatch": "^3.1.2",
|
||||
"object.entries": "^1.1.6",
|
||||
"object.fromentries": "^2.0.6",
|
||||
"object.hasown": "^1.1.2",
|
||||
"object.values": "^1.1.6",
|
||||
"prop-types": "^15.8.1",
|
||||
"resolve": "^2.0.0-next.4",
|
||||
"semver": "^6.3.0",
|
||||
"string.prototype.matchall": "^4.0.8"
|
||||
}
|
||||
},
|
||||
"glob": {
|
||||
"version": "7.1.7",
|
||||
"resolved": "https://registry.npmjs.org/glob/-/glob-7.1.7.tgz",
|
||||
"integrity": "sha512-OvD9ENzPLbegENnYP5UUfJIirTg4+XwMWGaQfQTY0JenxNvvIKP3U3/tAQSPIu/lHxXYSZmpXlUHeqAIdKzBLQ==",
|
||||
"requires": {
|
||||
"fs.realpath": "^1.0.0",
|
||||
"inflight": "^1.0.4",
|
||||
"inherits": "2",
|
||||
"minimatch": "^3.0.4",
|
||||
"once": "^1.3.0",
|
||||
"path-is-absolute": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"globby": {
|
||||
"version": "13.1.4",
|
||||
"resolved": "https://registry.npmjs.org/globby/-/globby-13.1.4.tgz",
|
||||
"integrity": "sha512-iui/IiiW+QrJ1X1hKH5qwlMQyv34wJAYwH1vrf8b9kBA4sNiif3gKsMHa+BrdnOpEudWjpotfa7LrTzB1ERS/g==",
|
||||
"requires": {
|
||||
"dir-glob": "^3.0.1",
|
||||
"fast-glob": "^3.2.11",
|
||||
"ignore": "^5.2.0",
|
||||
"merge2": "^1.4.1",
|
||||
"slash": "^4.0.0"
|
||||
}
|
||||
},
|
||||
"internmap": {
|
||||
"version": "1.0.1"
|
||||
},
|
||||
@@ -66429,6 +66750,21 @@
|
||||
"reduce-css-calc": "^2.1.8"
|
||||
}
|
||||
},
|
||||
"resolve": {
|
||||
"version": "2.0.0-next.4",
|
||||
"resolved": "https://registry.npmjs.org/resolve/-/resolve-2.0.0-next.4.tgz",
|
||||
"integrity": "sha512-iMDbmAWtfU+MHpxt/I5iWI7cY6YVEZUQ3MBgPQ++XD1PELuJHIl82xBmObyP2KyQmkNB2dsqF7seoQQiAn5yDQ==",
|
||||
"requires": {
|
||||
"is-core-module": "^2.9.0",
|
||||
"path-parse": "^1.0.7",
|
||||
"supports-preserve-symlinks-flag": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"slash": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/slash/-/slash-4.0.0.tgz",
|
||||
"integrity": "sha512-3dOsAHXXUkQTpOYcoAxLIorMTp4gIQr5IW3iVb7A7lFIp0VHhnynm9izx6TssdrIcVIESAlVjtnO2K8bg+Coew=="
|
||||
},
|
||||
"sql-formatter": {
|
||||
"version": "12.2.0",
|
||||
"resolved": "https://registry.npmjs.org/sql-formatter/-/sql-formatter-12.2.0.tgz",
|
||||
@@ -66440,7 +66776,7 @@
|
||||
},
|
||||
"typescript": {
|
||||
"version": "4.8.4",
|
||||
"dev": true
|
||||
"devOptional": true
|
||||
},
|
||||
"uuid": {
|
||||
"version": "8.3.2",
|
||||
@@ -66809,6 +67145,15 @@
|
||||
"integrity": "sha512-AsS729u2RHUfEra9xJrE39peJcc2stq2+poBXX8bcM08Y6g9j/i/PUzwNQqkaJde7Ntg1TO7bSREbR5sdosQ+g==",
|
||||
"dev": true
|
||||
},
|
||||
"synckit": {
|
||||
"version": "0.8.5",
|
||||
"resolved": "https://registry.npmjs.org/synckit/-/synckit-0.8.5.tgz",
|
||||
"integrity": "sha512-L1dapNV6vu2s/4Sputv8xGsCdAVlb5nRDMFU/E27D44l5U6cw1g0dGd45uLc+OXjNMmF4ntiMdCimzcjFKQI8Q==",
|
||||
"requires": {
|
||||
"@pkgr/utils": "^2.3.1",
|
||||
"tslib": "^2.5.0"
|
||||
}
|
||||
},
|
||||
"tabbable": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/tabbable/-/tabbable-4.0.0.tgz",
|
||||
@@ -67155,6 +67500,15 @@
|
||||
"resolved": "https://registry.npmjs.org/tiny-emitter/-/tiny-emitter-2.1.0.tgz",
|
||||
"integrity": "sha512-NB6Dk1A9xgQPMoGqC5CVXn123gWyte215ONT5Pp5a0yt4nlEoO1ZWeCwpncaekPHXO60i47ihFnZPiRPjRMq4Q=="
|
||||
},
|
||||
"tiny-glob": {
|
||||
"version": "0.2.9",
|
||||
"resolved": "https://registry.npmjs.org/tiny-glob/-/tiny-glob-0.2.9.tgz",
|
||||
"integrity": "sha512-g/55ssRPUjShh+xkfx9UPDXqhckHEsHr4Vd9zX55oSdGZc/MD0m3sferOkwWtp98bv+kcVfEHtRJgBVJzelrzg==",
|
||||
"requires": {
|
||||
"globalyzer": "0.1.0",
|
||||
"globrex": "^0.1.2"
|
||||
}
|
||||
},
|
||||
"tiny-invariant": {
|
||||
"version": "1.3.1",
|
||||
"resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.3.1.tgz",
|
||||
|
||||
@@ -60,6 +60,23 @@ export * from './src/components/Form'
|
||||
// CMD+K
|
||||
export * from './src/components/Command'
|
||||
|
||||
// layout
|
||||
|
||||
// banners
|
||||
export * from './src/layout/banners'
|
||||
|
||||
// config
|
||||
|
||||
// export { default as Config } from './../ui.config'
|
||||
|
||||
// ARCHIVE
|
||||
|
||||
// export * from './src/components/Textarea'
|
||||
|
||||
// AUTH
|
||||
|
||||
// export * from './src/components/Auth'
|
||||
|
||||
// ICONS
|
||||
// export icons
|
||||
export * from './src/components/Icon/icons/IconActivity'
|
||||
|
||||
@@ -3,8 +3,7 @@ import { useFormik, FormikConfig } from 'formik'
|
||||
import { FormContextProvider } from './FormContext'
|
||||
|
||||
// interface Props extends FormikProps<any>, Partial FormikConfig<any> {
|
||||
interface Props
|
||||
extends Omit<FormikConfig<any>, 'validateOnMount' | 'validateOnChange'> {
|
||||
interface Props extends Omit<FormikConfig<any>, 'validateOnMount' | 'validateOnChange'> {
|
||||
children: any
|
||||
handleIsSubmitting?: any
|
||||
handleIsValidating?: any
|
||||
@@ -69,8 +68,8 @@ export default function Form({ validate, ...props }: Props) {
|
||||
handleBlur={formik.handleBlur}
|
||||
touched={formik.touched}
|
||||
fieldLevelValidation={handleFieldLevelValidation}
|
||||
// children={rest.children}
|
||||
children={props.children({
|
||||
>
|
||||
{props.children({
|
||||
/** map of field names to specific error for that field */
|
||||
errors: formik.errors, // errors,
|
||||
// /** map of field names to whether the field has been touched */
|
||||
@@ -90,7 +89,7 @@ export default function Form({ validate, ...props }: Props) {
|
||||
/** Resets the form with custom values */
|
||||
resetForm: formik.resetForm,
|
||||
})}
|
||||
/>
|
||||
</FormContextProvider>
|
||||
</form>
|
||||
)
|
||||
}
|
||||
@@ -102,6 +102,9 @@ function Listbox({
|
||||
function handleResize() {
|
||||
// Set window width/height to state
|
||||
|
||||
// [Joshen] Note this causes some style conflicts if there are multiple listboxes
|
||||
// rendered on the same page. All listbox option widths will be that of the latest
|
||||
// listbox component that got rendered, rather than following its parent
|
||||
document.documentElement.style.setProperty(
|
||||
'--width-listbox',
|
||||
`${optionsWidth ? optionsWidth : triggerRef.current?.offsetWidth}px`
|
||||
|
||||
@@ -1,15 +1,18 @@
|
||||
import React from 'react'
|
||||
import Link from 'next/link'
|
||||
import Countdown from 'react-countdown'
|
||||
import _announcement from '~/data/Announcement.json'
|
||||
import { AnnouncementProps } from '../../Nav/Announcement'
|
||||
import { useRouter } from 'next/router'
|
||||
import { PropsWithChildren } from 'react'
|
||||
import Countdown from 'react-countdown'
|
||||
import { AnnouncementProps } from './Announcement'
|
||||
import _announcement from './data/Announcement.json'
|
||||
|
||||
interface CountdownStepProps {
|
||||
value: string | number
|
||||
unit: string
|
||||
}
|
||||
|
||||
interface CountdownButtonProps {
|
||||
dest: string
|
||||
}
|
||||
|
||||
const announcement = _announcement as AnnouncementProps
|
||||
|
||||
function CountdownStep({ value, unit }: CountdownStepProps) {
|
||||
@@ -23,6 +26,17 @@ function CountdownStep({ value, unit }: CountdownStepProps) {
|
||||
)
|
||||
}
|
||||
|
||||
function CountdownButton({ children, dest }: PropsWithChildren<CountdownButtonProps>) {
|
||||
return (
|
||||
<a
|
||||
href={dest}
|
||||
className="bg-white text-xs px-1.5 md:px-2.5 py-1 rounded-full text-[#9E44EF] shadow-none hover:shadow-mg cursor-pointer"
|
||||
>
|
||||
{children}
|
||||
</a>
|
||||
)
|
||||
}
|
||||
|
||||
function CountdownBanner() {
|
||||
const { pathname } = useRouter()
|
||||
const isLaunchWeekPage = pathname === '/launch-week'
|
||||
@@ -35,11 +49,7 @@ function CountdownBanner() {
|
||||
<div className="w-full flex gap-3 md:gap-6 items-center justify-center">
|
||||
<p>Supabase Launch Week 7</p>
|
||||
<div>
|
||||
<Link href="/launch-week">
|
||||
<a className="bg-white text-xs px-1.5 md:px-2.5 py-1 rounded-full text-[#9E44EF] shadow-none hover:shadow-mg cursor-pointer">
|
||||
Explore
|
||||
</a>
|
||||
</Link>
|
||||
<CountdownButton dest="/launch-week">Explore</CountdownButton>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
@@ -63,11 +73,7 @@ function CountdownBanner() {
|
||||
</div>
|
||||
{!isLaunchWeekPage && (
|
||||
<div className="hidden md:block">
|
||||
<Link href="/launch-week">
|
||||
<a className="bg-white text-xs px-1.5 md:px-2.5 py-1 rounded-full text-[#9E44EF] shadow-none hover:shadow-mg cursor-pointer">
|
||||
Get your ticket
|
||||
</a>
|
||||
</Link>
|
||||
<CountdownButton dest="/launch-week">Get your ticket</CountdownButton>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
@@ -1,6 +1,6 @@
|
||||
import React, { useEffect, useState } from 'react'
|
||||
import { useEffect, useState } from 'react'
|
||||
|
||||
import _announcement from '~/data/Announcement.json'
|
||||
import _announcement from './data/Announcement.json'
|
||||
import { IconX } from 'ui'
|
||||
import { useRouter } from 'next/router'
|
||||
import { PropsWithChildren } from 'react'
|
||||
@@ -51,16 +51,15 @@ const Announcement = ({
|
||||
return setHidden(true)
|
||||
}
|
||||
|
||||
function handleLink() {
|
||||
router.push(announcement.link)
|
||||
}
|
||||
|
||||
// Always show if on LW section
|
||||
if (!isLaunchWeekSection && hidden) {
|
||||
return null
|
||||
} else {
|
||||
return (
|
||||
<div onClick={handleLink} className={['relative w-full cursor-pointer', className].join(' ')}>
|
||||
<div
|
||||
onClick={() => window.location.assign(announcement.link)}
|
||||
className={['relative w-full cursor-pointer', className].join(' ')}
|
||||
>
|
||||
{!isLaunchWeekSection && (
|
||||
<div
|
||||
className="absolute right-4 flex h-full items-center opacity-50 transition-opacity hover:opacity-100"
|
||||
@@ -2,6 +2,6 @@
|
||||
"show": true,
|
||||
"text": "Supabase Launch Week 7",
|
||||
"launchDate": "2023-04-10T07:00:00.000-07:00",
|
||||
"link": "/launch-week",
|
||||
"link": "https://supabase.com/launch-week",
|
||||
"badge": "Get your ticket"
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
export { default as Announcement } from './Announcement'
|
||||
export { default as AnnouncementCountdown } from './Announcement.Countdown'
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"extends": "next/core-web-vitals",
|
||||
"rules": {
|
||||
"@next/next/no-img-element": "off",
|
||||
"react/no-unescaped-entities": "off",
|
||||
"react/display-name": "warn"
|
||||
}
|
||||
}
|
||||
@@ -32,6 +32,8 @@ export const SelectColumn: CalculatedColumn<any, any> = {
|
||||
)
|
||||
},
|
||||
formatter: (props: FormatterProps<SupaRow>) => {
|
||||
// [Alaister] formatter is actually a valid React component, so we can use hooks here
|
||||
// eslint-disable-next-line react-hooks/rules-of-hooks
|
||||
const [isRowSelected, onRowSelectionChange] = useRowSelection()
|
||||
return (
|
||||
<SelectCellFormatter
|
||||
@@ -49,6 +51,8 @@ export const SelectColumn: CalculatedColumn<any, any> = {
|
||||
)
|
||||
},
|
||||
groupFormatter: (props: GroupFormatterProps<SupaRow>) => {
|
||||
// [Alaister] groupFormatter is actually a valid React component, so we can use hooks here
|
||||
// eslint-disable-next-line react-hooks/rules-of-hooks
|
||||
const [isRowSelected, onRowSelectionChange] = useRowSelection()
|
||||
return (
|
||||
<SelectCellFormatter
|
||||
|
||||
@@ -58,12 +58,11 @@ export default function RLSBannerWarning() {
|
||||
size="medium"
|
||||
onSelectCancel={() => setIsOpen(false)}
|
||||
onSelectConfirm={handleDismissWarning}
|
||||
children={
|
||||
<Modal.Content>
|
||||
<RLSDisableModalContent />
|
||||
</Modal.Content>
|
||||
}
|
||||
/>
|
||||
>
|
||||
<Modal.Content>
|
||||
<RLSDisableModalContent />
|
||||
</Modal.Content>
|
||||
</ConfirmationModal>
|
||||
</div>
|
||||
) : (
|
||||
<></>
|
||||
|
||||
@@ -17,7 +17,6 @@ export interface SortRowProps {
|
||||
|
||||
const SortRow = ({ table, index, columnName, sort, onDelete, onToggle, onDrag }: SortRowProps) => {
|
||||
const column = table.columns.find((x) => x.name === columnName)
|
||||
if (!column) return null
|
||||
|
||||
const ref = useRef<HTMLDivElement>(null)
|
||||
|
||||
@@ -95,6 +94,8 @@ const SortRow = ({ table, index, columnName, sort, onDelete, onToggle, onDrag }:
|
||||
const opacity = isDragging ? 0 : 1
|
||||
drag(drop(ref))
|
||||
|
||||
if (!column) return null
|
||||
|
||||
return (
|
||||
<div
|
||||
className="flex items-center gap-3 px-3"
|
||||
|
||||
@@ -7,6 +7,9 @@ import { useDispatch } from 'components/grid/store'
|
||||
|
||||
export class SqlRowService implements IRowService {
|
||||
protected query = new Query()
|
||||
// [Alaister]: This store is going to be removed in the near future
|
||||
// and I really don't want to fix this "hook inside class" craziness 🤯
|
||||
// eslint-disable-next-line react-hooks/rules-of-hooks
|
||||
protected dispatch = useDispatch()
|
||||
|
||||
constructor(
|
||||
|
||||
@@ -89,14 +89,13 @@ const AccessTokenList = observer(() => {
|
||||
onSelectConfirm={() => {
|
||||
if (token) onDeleteToken(token.id)
|
||||
}}
|
||||
children={
|
||||
<Modal.Content>
|
||||
<p className="py-4 text-sm text-scale-1100">
|
||||
{`This action cannot be undone. Are you sure you want to delete "${token?.name}" token?`}
|
||||
</p>
|
||||
</Modal.Content>
|
||||
}
|
||||
/>
|
||||
>
|
||||
<Modal.Content>
|
||||
<p className="py-4 text-sm text-scale-1100">
|
||||
{`This action cannot be undone. Are you sure you want to delete "${token?.name}" token?`}
|
||||
</p>
|
||||
</Modal.Content>
|
||||
</ConfirmationModal>
|
||||
</>
|
||||
)
|
||||
})
|
||||
|
||||
@@ -115,7 +115,7 @@ const NewAccessTokenButton = observer(({ onCreateToken }: NewAccessTokenButtonPr
|
||||
</p>
|
||||
<div className="mt-4">
|
||||
<Link href="https://api.supabase.com/api/v0">
|
||||
<a target="_blank">
|
||||
<a target="_blank" rel="noreferrer">
|
||||
<Button type="default" icon={<IconExternalLink strokeWidth={1.5} />}>
|
||||
Experimental API documentation
|
||||
</Button>
|
||||
|
||||
@@ -10,7 +10,7 @@ const AppBannerWrapper: FC = ({ children }) => {
|
||||
<div className="min-h-full flex flex-col">
|
||||
{ongoingIncident && (
|
||||
<Link href="https://status.supabase.com">
|
||||
<a target="_blank">
|
||||
<a target="_blank" rel="noreferrer">
|
||||
<div className="flex cursor-pointer items-center justify-center space-x-2 bg-green-900 py-3 text-scale-400 transition hover:bg-green-1000 dark:text-scale-1200">
|
||||
<p className="text-sm font-medium">
|
||||
We are currently investigating a technical issue, follow status.supabase.com for
|
||||
|
||||
@@ -12,10 +12,10 @@ interface Props {
|
||||
}
|
||||
|
||||
const FormField: FC<Props> = ({ name, properties, formValues, disabled = false }) => {
|
||||
if (properties.show && formValues[properties.show.key] !== properties.show.matches) return <></>
|
||||
|
||||
const [hidden, setHidden] = useState(!!properties.isSecret)
|
||||
|
||||
if (properties.show && formValues[properties.show.key] !== properties.show.matches) return null
|
||||
|
||||
switch (properties.type) {
|
||||
case 'string':
|
||||
return (
|
||||
|
||||
@@ -15,7 +15,11 @@ const EmailTemplates = observer(() => {
|
||||
/>
|
||||
<div className="mb-6">
|
||||
<Button type="default" icon={<IconExternalLink size={14} strokeWidth={1.5} />}>
|
||||
<a target="_blank" href="https://supabase.com/docs/guides/auth/auth-email-templates">
|
||||
<a
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
href="https://supabase.com/docs/guides/auth/auth-email-templates"
|
||||
>
|
||||
Documentation
|
||||
</a>
|
||||
</Button>
|
||||
|
||||
@@ -64,6 +64,9 @@ const TemplateEditor: FC<Props> = ({ template }) => {
|
||||
return (
|
||||
<Form id={formId} className="!border-t-0" initialValues={INITIAL_VALUES} onSubmit={onSubmit}>
|
||||
{({ isSubmitting, resetForm, values, initialValues }: any) => {
|
||||
// [Alaister] although this "technically" is breaking the rules of React hooks
|
||||
// it won't error because the hooks are always rendered in the same order
|
||||
// eslint-disable-next-line react-hooks/rules-of-hooks
|
||||
useEffect(() => {
|
||||
// Form is reset once remote data is loaded in store
|
||||
resetForm({
|
||||
@@ -85,7 +88,7 @@ const TemplateEditor: FC<Props> = ({ template }) => {
|
||||
const property = properties[x]
|
||||
if (property.type === 'string') {
|
||||
return (
|
||||
<div className="space-y-3">
|
||||
<div key={x} className="space-y-3">
|
||||
<label className="col-span-12 text-sm text-scale-1200 lg:col-span-5">
|
||||
{property.title}
|
||||
</label>
|
||||
|
||||
@@ -9,8 +9,14 @@ interface Props {
|
||||
|
||||
const PolicyEditorFooter: FC<Props> = ({ showTemplates, onViewTemplates, onReviewPolicy }) => (
|
||||
<div className="flex justify-between items-center border-t px-6 py-4 dark:border-dark">
|
||||
<a href='https://supabase.com/docs/learn/auth-deep-dive/auth-policies' target="_blank">
|
||||
<Button type='link' icon={<IconExternalLink size={14} strokeWidth={1.5} />}>Documentation</Button>
|
||||
<a
|
||||
href="https://supabase.com/docs/learn/auth-deep-dive/auth-policies"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
<Button type="link" icon={<IconExternalLink size={14} strokeWidth={1.5} />}>
|
||||
Documentation
|
||||
</Button>
|
||||
</a>
|
||||
<div className="flex w-full items-center justify-end gap-2">
|
||||
{showTemplates && (
|
||||
|
||||
@@ -61,6 +61,7 @@ const PolicySelection: FC<Props> = ({
|
||||
Not sure what policies are? Check out our resources{' '}
|
||||
<a
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
className="text-brand-900 transition-colors hover:text-brand-1200"
|
||||
href="https://supabase.com/docs/guides/auth#policies"
|
||||
>
|
||||
|
||||
@@ -125,6 +125,9 @@ const SmtpForm = () => {
|
||||
const isValidSmtpConfig = isSmtpEnabled(values)
|
||||
const hasChanges = JSON.stringify(values) !== JSON.stringify(initialValues)
|
||||
|
||||
// [Alaister] although this "technically" is breaking the rules of React hooks
|
||||
// it won't error because the hooks are always rendered in the same order
|
||||
// eslint-disable-next-line react-hooks/rules-of-hooks
|
||||
useEffect(() => {
|
||||
if (isLoaded) {
|
||||
const formValues = generateFormValues(config)
|
||||
|
||||
@@ -37,7 +37,7 @@ const ComputeSizeSelection: FC<Props> = ({
|
||||
</p>
|
||||
</div>
|
||||
<Link href="https://supabase.com/docs/guides/platform/compute-add-ons">
|
||||
<a target="_blank">
|
||||
<a target="_blank" rel="noreferrer">
|
||||
<Button type="default" icon={<IconExternalLink strokeWidth={1.5} />}>
|
||||
About compute add-ons
|
||||
</Button>
|
||||
|
||||
@@ -31,7 +31,11 @@ export const USAGE_BASED_PRODUCTS = [
|
||||
tooltip: (
|
||||
<span>
|
||||
Billing is based on the average daily database size in GB throughout the billing period.{' '}
|
||||
<a href="https://supabase.com/docs/guides/platform/database-usage" target="_blank">
|
||||
<a
|
||||
href="https://supabase.com/docs/guides/platform/database-usage"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
Docs
|
||||
</a>
|
||||
</span>
|
||||
|
||||
@@ -131,7 +131,7 @@ const Invoices: FC<Props> = ({ projectRef }) => {
|
||||
<div className="flex items-center justify-end space-x-2">
|
||||
{[InvoiceStatus.UNCOLLECTIBLE, InvoiceStatus.OPEN].includes(x.status) && (
|
||||
<Link href={`https://redirect.revops.supabase.com/pay-invoice/${x.id}`}>
|
||||
<a target="_blank">
|
||||
<a target="_blank" rel="noreferrer">
|
||||
<Button>Pay Now</Button>
|
||||
</a>
|
||||
</Link>
|
||||
|
||||
@@ -45,7 +45,7 @@ const PlanSelection: FC<Props> = ({ visible, tiers, currentPlan, onSelectPlan })
|
||||
</div>
|
||||
<div className="flex justify-center items-center mt-4">
|
||||
<Link href="https://supabase.com/pricing">
|
||||
<a target="_blank">
|
||||
<a target="_blank" rel="noreferrer">
|
||||
<Button
|
||||
type="link"
|
||||
icon={<IconExternalLink size={14} strokeWidth={1.5} />}
|
||||
|
||||
@@ -15,7 +15,7 @@ interface Props {
|
||||
const PlanCard: FC<Props> = ({ plan, currentPlan, onSelectPlan }) => {
|
||||
// Team tier is enabled when the flag is turned on OR the user is already on the team tier (manually assigned by us)
|
||||
const teamTierEnabled =
|
||||
currentPlan?.supabase_prod_id === PRICING_TIER_PRODUCT_IDS.TEAM || useFlag('teamTier')
|
||||
useFlag('teamTier') || currentPlan?.supabase_prod_id === PRICING_TIER_PRODUCT_IDS.TEAM
|
||||
|
||||
const planMeta = PRICING_META[plan.id]
|
||||
const isEnterprise = plan.id === 'Enterprise'
|
||||
@@ -134,9 +134,7 @@ const PlanCard: FC<Props> = ({ plan, currentPlan, onSelectPlan }) => {
|
||||
|
||||
{planMeta.name === 'Pro' && currentPlan?.prod_id === STRIPE_PRODUCT_IDS.PAYG && (
|
||||
<div>
|
||||
<p className="text-sm text-scale-1100">
|
||||
Spend cap is currently disabled
|
||||
</p>
|
||||
<p className="text-sm text-scale-1100">Spend cap is currently disabled</p>
|
||||
<p className="text-xs text-scale-1000">
|
||||
You will be charged for usage beyond the plan limits
|
||||
</p>{' '}
|
||||
|
||||
@@ -129,7 +129,7 @@ const SpendCapModal: FC<Props> = ({ visible, onHide }) => {
|
||||
<p>
|
||||
See{' '}
|
||||
<Link href="https://supabase.com/pricing" passHref>
|
||||
<a className="text-brand-900" target="_blank">
|
||||
<a className="text-brand-900" target="_blank" rel="noreferrer">
|
||||
<span>pricing page</span>
|
||||
</a>
|
||||
</Link>{' '}
|
||||
|
||||
@@ -150,7 +150,12 @@ const Subscription: FC<Props> = ({ showProjectName = false }) => {
|
||||
<div className="px-6 pb-4 mt-2">
|
||||
<p className="text-sm text-scale-1100">
|
||||
See our{' '}
|
||||
<a href="https://supabase.com/pricing" target="_blank" className="underline">
|
||||
<a
|
||||
href="https://supabase.com/pricing"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
className="underline"
|
||||
>
|
||||
pricing
|
||||
</a>{' '}
|
||||
for a more detailed analysis of what Supabase has on offer.
|
||||
|
||||
@@ -52,7 +52,7 @@ const TeamUpgrade: FC<Props> = ({
|
||||
// Team tier is enabled when the flag is turned on OR the user is already on the team tier (manually assigned by us)
|
||||
const userIsOnTeamTier =
|
||||
currentSubscription?.tier?.supabase_prod_id === PRICING_TIER_PRODUCT_IDS.TEAM
|
||||
const teamTierEnabled = userIsOnTeamTier || useFlag('teamTier')
|
||||
const teamTierEnabled = useFlag('teamTier') || userIsOnTeamTier
|
||||
|
||||
const [captchaToken, setCaptchaToken] = useState<string | null>(null)
|
||||
const captchaRef = useRef<HCaptcha>(null)
|
||||
|
||||
@@ -451,15 +451,14 @@ const CreateFunction: FC<CreateFunctionProps> = ({ func, visible, setVisible })
|
||||
setIsClosingPanel(false)
|
||||
setVisible(!visible)
|
||||
}}
|
||||
children={
|
||||
<Modal.Content>
|
||||
<p className="py-4 text-sm text-scale-1100">
|
||||
There are unsaved changes. Are you sure you want to close the panel? Your changes
|
||||
will be lost.
|
||||
</p>
|
||||
</Modal.Content>
|
||||
}
|
||||
/>
|
||||
>
|
||||
<Modal.Content>
|
||||
<p className="py-4 text-sm text-scale-1100">
|
||||
There are unsaved changes. Are you sure you want to close the panel? Your changes will
|
||||
be lost.
|
||||
</p>
|
||||
</Modal.Content>
|
||||
</ConfirmationModal>
|
||||
</SidePanel>
|
||||
</>
|
||||
)
|
||||
|
||||
@@ -63,7 +63,6 @@ const FunctionsList: FC<Props> = ({
|
||||
disabled={!canCreateFunctions}
|
||||
disabledMessage="You need additional permissions to create functions"
|
||||
>
|
||||
<AlphaPreview />
|
||||
<p className="text-sm text-scale-1100">
|
||||
PostgreSQL functions, also known as stored procedures, is a set of SQL and procedural
|
||||
commands such as declarations, assignments, loops, flow-of-control, etc.
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import Image from 'next/image'
|
||||
import { useState, useRef, useEffect } from 'react'
|
||||
import { PostgresTrigger } from '@supabase/postgres-meta'
|
||||
import { useState, useRef, useEffect, MutableRefObject } from 'react'
|
||||
import { PostgresTable, PostgresTrigger } from '@supabase/postgres-meta'
|
||||
import { Button, SidePanel, Form, Input, Listbox, Checkbox, Radio, Badge, Modal } from 'ui'
|
||||
|
||||
import { useStore } from 'hooks'
|
||||
@@ -13,7 +13,10 @@ import ConfirmationModal from 'components/ui/ConfirmationModal'
|
||||
import { FormSection, FormSectionLabel, FormSectionContent } from 'components/ui/Forms'
|
||||
import { useProjectContext } from 'components/layouts/ProjectLayout/ProjectContext'
|
||||
|
||||
import { useEdgeFunctionsQuery } from 'data/edge-functions/edge-functions-query'
|
||||
import {
|
||||
EdgeFunctionsResponse,
|
||||
useEdgeFunctionsQuery,
|
||||
} from 'data/edge-functions/edge-functions-query'
|
||||
import { useDatabaseTriggerCreateMutation } from 'data/database-triggers/database-trigger-create-mutation'
|
||||
import { useDatabaseTriggerUpdateMutation } from 'data/database-triggers/database-trigger-update-transaction-mutation'
|
||||
|
||||
@@ -29,7 +32,7 @@ const EditHookPanel = ({ visible, selectedHook, onClose }: EditHookPanelProps) =
|
||||
// [Joshen] Need to change to use RQ once Alaister's PR goes in
|
||||
const { ref } = useParams()
|
||||
const { meta, ui } = useStore()
|
||||
const submitRef: any = useRef()
|
||||
const submitRef = useRef<any>(null)
|
||||
const [isEdited, setIsEdited] = useState(false)
|
||||
const [isSubmitting, setIsSubmitting] = useState(false)
|
||||
const [isClosingPanel, setIsClosingPanel] = useState(false)
|
||||
@@ -278,181 +281,26 @@ const EditHookPanel = ({ visible, selectedHook, onClose }: EditHookPanelProps) =
|
||||
>
|
||||
<Form validateOnBlur initialValues={initialValues} onSubmit={onSubmit} validate={validate}>
|
||||
{({ values, resetForm, errors }: any) => {
|
||||
useEffect(() => {
|
||||
if (values.function_type === 'http_request') {
|
||||
if (selectedHook !== undefined) {
|
||||
const [url, method] = selectedHook.function_args
|
||||
const updatedValues = { ...values, http_url: url, http_method: method }
|
||||
resetForm({ values: updatedValues, initialValues: updatedValues })
|
||||
} else {
|
||||
const updatedValues = { ...values, http_url: '' }
|
||||
resetForm({ values: updatedValues, initialValues: updatedValues })
|
||||
}
|
||||
} else if (values.function_type === 'supabase_function') {
|
||||
const fnSlug = (functions ?? [])[0]?.slug
|
||||
const defaultFunctionUrl = `https://${ref}.functions.supabase.${restUrlTld}/${fnSlug}`
|
||||
const updatedValues = {
|
||||
...values,
|
||||
http_url: isEdgeFunction(values.http_url) ? values.http_url : defaultFunctionUrl,
|
||||
}
|
||||
resetForm({ values: updatedValues, initialValues: updatedValues })
|
||||
}
|
||||
}, [values.function_type])
|
||||
|
||||
return (
|
||||
<div>
|
||||
<FormSection
|
||||
header={<FormSectionLabel className="lg:!col-span-4">General</FormSectionLabel>}
|
||||
>
|
||||
<FormSectionContent loading={false} className="lg:!col-span-8">
|
||||
<Input
|
||||
id="name"
|
||||
name="name"
|
||||
label="Name"
|
||||
descriptionText="Do not use spaces/whitespaces"
|
||||
/>
|
||||
</FormSectionContent>
|
||||
</FormSection>
|
||||
<SidePanel.Separator />
|
||||
<FormSection
|
||||
header={
|
||||
<FormSectionLabel
|
||||
className="lg:!col-span-4"
|
||||
description={
|
||||
<p className="text-sm text-scale-1000">
|
||||
Select which table and events will trigger your webhook
|
||||
</p>
|
||||
}
|
||||
>
|
||||
Conditions to fire webhook
|
||||
</FormSectionLabel>
|
||||
}
|
||||
>
|
||||
<FormSectionContent loading={false} className="lg:!col-span-8">
|
||||
<Listbox
|
||||
size="medium"
|
||||
id="table_id"
|
||||
name="table_id"
|
||||
label="Table"
|
||||
descriptionText="This is the table the trigger will watch for changes. You can only select 1 table for a trigger."
|
||||
>
|
||||
<Listbox.Option
|
||||
key={'table-no-selection'}
|
||||
id={'table-no-selection'}
|
||||
label={'---'}
|
||||
value={'no-selection'}
|
||||
>
|
||||
---
|
||||
</Listbox.Option>
|
||||
{tables.map((table) => (
|
||||
<Listbox.Option
|
||||
key={table.id}
|
||||
id={table.id.toString()}
|
||||
value={table.id}
|
||||
label={table.name}
|
||||
>
|
||||
<div className="flex items-center space-x-2">
|
||||
<p className="text-scale-1000">{table.schema}</p>
|
||||
<p className="text-scale-1200">{table.name}</p>
|
||||
</div>
|
||||
</Listbox.Option>
|
||||
))}
|
||||
</Listbox>
|
||||
<Checkbox.Group
|
||||
id="events"
|
||||
name="events"
|
||||
label="Events"
|
||||
error={eventsError}
|
||||
descriptionText="These are the events that are watched by the webhook, only the events selected above will fire the webhook on the table you've selected."
|
||||
>
|
||||
{HOOK_EVENTS.map((event) => (
|
||||
<Checkbox
|
||||
key={event.value}
|
||||
value={event.value}
|
||||
label={event.label}
|
||||
description={event.description}
|
||||
checked={events.includes(event.value)}
|
||||
onChange={() => onUpdateSelectedEvents(event.value)}
|
||||
/>
|
||||
))}
|
||||
</Checkbox.Group>
|
||||
</FormSectionContent>
|
||||
</FormSection>
|
||||
<SidePanel.Separator />
|
||||
<FormSection
|
||||
header={
|
||||
<FormSectionLabel className="lg:!col-span-4">
|
||||
Webhook configuration
|
||||
</FormSectionLabel>
|
||||
}
|
||||
>
|
||||
<FormSectionContent loading={false} className="lg:!col-span-8">
|
||||
<Radio.Group
|
||||
id="function_type"
|
||||
name="function_type"
|
||||
label="Type of webhook"
|
||||
type="cards"
|
||||
>
|
||||
{AVAILABLE_WEBHOOK_TYPES.map((webhook) => (
|
||||
<Radio
|
||||
key={webhook.value}
|
||||
id={webhook.value}
|
||||
value={webhook.value}
|
||||
label=""
|
||||
beforeLabel={
|
||||
<div className="flex items-center space-x-5">
|
||||
<Image src={webhook.icon} layout="fixed" width="32" height="32" />
|
||||
<div className="flex-col space-y-0">
|
||||
<div className="flex space-x-2">
|
||||
<p className="text-scale-1200">{webhook.label}</p>
|
||||
</div>
|
||||
<p className="text-scale-1000">{webhook.description}</p>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
))}
|
||||
</Radio.Group>
|
||||
</FormSectionContent>
|
||||
</FormSection>
|
||||
<SidePanel.Separator />
|
||||
|
||||
<HTTPRequestFields
|
||||
type={values.function_type}
|
||||
errors={errors}
|
||||
httpHeaders={httpHeaders}
|
||||
httpParameters={httpParameters}
|
||||
onAddHeader={(header?: any) => {
|
||||
if (header) setHttpHeaders(httpHeaders.concat(header))
|
||||
else setHttpHeaders(httpHeaders.concat({ id: uuidv4(), name: '', value: '' }))
|
||||
}}
|
||||
onUpdateHeader={(idx, property, value) =>
|
||||
setHttpHeaders(
|
||||
httpHeaders.map((header, i) => {
|
||||
if (idx === i) return { ...header, [property]: value }
|
||||
else return header
|
||||
})
|
||||
)
|
||||
}
|
||||
onRemoveHeader={(idx) => setHttpHeaders(httpHeaders.filter((_, i) => idx !== i))}
|
||||
onAddParameter={() =>
|
||||
setHttpParameters(httpParameters.concat({ id: uuidv4(), name: '', value: '' }))
|
||||
}
|
||||
onUpdateParameter={(idx, property, value) =>
|
||||
setHttpParameters(
|
||||
httpParameters.map((param, i) => {
|
||||
if (idx === i) return { ...param, [property]: value }
|
||||
else return param
|
||||
})
|
||||
)
|
||||
}
|
||||
onRemoveParameter={(idx) =>
|
||||
setHttpParameters(httpParameters.filter((_, i) => idx !== i))
|
||||
}
|
||||
/>
|
||||
|
||||
<button ref={submitRef} type="submit" className="hidden" />
|
||||
</div>
|
||||
<FormContents
|
||||
values={values}
|
||||
resetForm={resetForm}
|
||||
errors={errors}
|
||||
projectRef={ref}
|
||||
restUrlTld={restUrlTld}
|
||||
functions={functions}
|
||||
isEdgeFunction={isEdgeFunction}
|
||||
tables={tables}
|
||||
events={events}
|
||||
eventsError={eventsError}
|
||||
onUpdateSelectedEvents={onUpdateSelectedEvents}
|
||||
httpHeaders={httpHeaders}
|
||||
httpParameters={httpParameters}
|
||||
setHttpHeaders={setHttpHeaders}
|
||||
setHttpParameters={setHttpParameters}
|
||||
submitRef={submitRef}
|
||||
selectedHook={selectedHook}
|
||||
/>
|
||||
)
|
||||
}}
|
||||
</Form>
|
||||
@@ -467,17 +315,222 @@ const EditHookPanel = ({ visible, selectedHook, onClose }: EditHookPanelProps) =
|
||||
setIsEdited(false)
|
||||
onClose()
|
||||
}}
|
||||
children={
|
||||
<Modal.Content>
|
||||
<p className="py-4 text-sm text-scale-1100">
|
||||
There are unsaved changes. Are you sure you want to close the panel? Your changes will
|
||||
be lost.
|
||||
</p>
|
||||
</Modal.Content>
|
||||
}
|
||||
/>
|
||||
>
|
||||
<Modal.Content>
|
||||
<p className="py-4 text-sm text-scale-1100">
|
||||
There are unsaved changes. Are you sure you want to close the panel? Your changes will
|
||||
be lost.
|
||||
</p>
|
||||
</Modal.Content>
|
||||
</ConfirmationModal>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default EditHookPanel
|
||||
|
||||
interface FormContentsProps {
|
||||
values: any
|
||||
resetForm: any
|
||||
errors: any
|
||||
projectRef?: string
|
||||
restUrlTld?: string
|
||||
selectedHook?: PostgresTrigger
|
||||
functions: EdgeFunctionsResponse[] | undefined
|
||||
isEdgeFunction: (url: string) => boolean
|
||||
tables: PostgresTable[]
|
||||
events: string[]
|
||||
eventsError?: string
|
||||
onUpdateSelectedEvents: (event: string) => void
|
||||
httpHeaders: HTTPArgument[]
|
||||
httpParameters: HTTPArgument[]
|
||||
setHttpHeaders: (arr: HTTPArgument[]) => void
|
||||
setHttpParameters: (arr: HTTPArgument[]) => void
|
||||
submitRef: MutableRefObject<any>
|
||||
}
|
||||
|
||||
const FormContents = ({
|
||||
values,
|
||||
resetForm,
|
||||
errors,
|
||||
projectRef,
|
||||
restUrlTld,
|
||||
selectedHook,
|
||||
functions,
|
||||
isEdgeFunction,
|
||||
tables,
|
||||
events,
|
||||
eventsError,
|
||||
onUpdateSelectedEvents,
|
||||
httpHeaders,
|
||||
httpParameters,
|
||||
setHttpHeaders,
|
||||
setHttpParameters,
|
||||
submitRef,
|
||||
}: FormContentsProps) => {
|
||||
useEffect(() => {
|
||||
if (values.function_type === 'http_request') {
|
||||
if (selectedHook !== undefined) {
|
||||
const [url, method] = selectedHook.function_args
|
||||
const updatedValues = { ...values, http_url: url, http_method: method }
|
||||
resetForm({ values: updatedValues, initialValues: updatedValues })
|
||||
} else {
|
||||
const updatedValues = { ...values, http_url: '' }
|
||||
resetForm({ values: updatedValues, initialValues: updatedValues })
|
||||
}
|
||||
} else if (values.function_type === 'supabase_function') {
|
||||
const fnSlug = (functions ?? [])[0]?.slug
|
||||
const defaultFunctionUrl = `https://${projectRef}.functions.supabase.${restUrlTld}/${fnSlug}`
|
||||
const updatedValues = {
|
||||
...values,
|
||||
http_url: isEdgeFunction(values.http_url) ? values.http_url : defaultFunctionUrl,
|
||||
}
|
||||
resetForm({ values: updatedValues, initialValues: updatedValues })
|
||||
}
|
||||
}, [values.function_type])
|
||||
|
||||
return (
|
||||
<div>
|
||||
<FormSection header={<FormSectionLabel className="lg:!col-span-4">General</FormSectionLabel>}>
|
||||
<FormSectionContent loading={false} className="lg:!col-span-8">
|
||||
<Input
|
||||
id="name"
|
||||
name="name"
|
||||
label="Name"
|
||||
descriptionText="Do not use spaces/whitespaces"
|
||||
/>
|
||||
</FormSectionContent>
|
||||
</FormSection>
|
||||
<SidePanel.Separator />
|
||||
<FormSection
|
||||
header={
|
||||
<FormSectionLabel
|
||||
className="lg:!col-span-4"
|
||||
description={
|
||||
<p className="text-sm text-scale-1000">
|
||||
Select which table and events will trigger your webhook
|
||||
</p>
|
||||
}
|
||||
>
|
||||
Conditions to fire webhook
|
||||
</FormSectionLabel>
|
||||
}
|
||||
>
|
||||
<FormSectionContent loading={false} className="lg:!col-span-8">
|
||||
<Listbox
|
||||
size="medium"
|
||||
id="table_id"
|
||||
name="table_id"
|
||||
label="Table"
|
||||
descriptionText="This is the table the trigger will watch for changes. You can only select 1 table for a trigger."
|
||||
>
|
||||
<Listbox.Option
|
||||
key={'table-no-selection'}
|
||||
id={'table-no-selection'}
|
||||
label={'---'}
|
||||
value={'no-selection'}
|
||||
>
|
||||
---
|
||||
</Listbox.Option>
|
||||
{tables.map((table) => (
|
||||
<Listbox.Option
|
||||
key={table.id}
|
||||
id={table.id.toString()}
|
||||
value={table.id}
|
||||
label={table.name}
|
||||
>
|
||||
<div className="flex items-center space-x-2">
|
||||
<p className="text-scale-1000">{table.schema}</p>
|
||||
<p className="text-scale-1200">{table.name}</p>
|
||||
</div>
|
||||
</Listbox.Option>
|
||||
))}
|
||||
</Listbox>
|
||||
<Checkbox.Group
|
||||
id="events"
|
||||
name="events"
|
||||
label="Events"
|
||||
error={eventsError}
|
||||
descriptionText="These are the events that are watched by the webhook, only the events selected above will fire the webhook on the table you've selected."
|
||||
>
|
||||
{HOOK_EVENTS.map((event) => (
|
||||
<Checkbox
|
||||
key={event.value}
|
||||
value={event.value}
|
||||
label={event.label}
|
||||
description={event.description}
|
||||
checked={events.includes(event.value)}
|
||||
onChange={() => onUpdateSelectedEvents(event.value)}
|
||||
/>
|
||||
))}
|
||||
</Checkbox.Group>
|
||||
</FormSectionContent>
|
||||
</FormSection>
|
||||
<SidePanel.Separator />
|
||||
<FormSection
|
||||
header={
|
||||
<FormSectionLabel className="lg:!col-span-4">Webhook configuration</FormSectionLabel>
|
||||
}
|
||||
>
|
||||
<FormSectionContent loading={false} className="lg:!col-span-8">
|
||||
<Radio.Group id="function_type" name="function_type" label="Type of webhook" type="cards">
|
||||
{AVAILABLE_WEBHOOK_TYPES.map((webhook) => (
|
||||
<Radio
|
||||
key={webhook.value}
|
||||
id={webhook.value}
|
||||
value={webhook.value}
|
||||
label=""
|
||||
beforeLabel={
|
||||
<div className="flex items-center space-x-5">
|
||||
<Image src={webhook.icon} layout="fixed" width="32" height="32" />
|
||||
<div className="flex-col space-y-0">
|
||||
<div className="flex space-x-2">
|
||||
<p className="text-scale-1200">{webhook.label}</p>
|
||||
</div>
|
||||
<p className="text-scale-1000">{webhook.description}</p>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
))}
|
||||
</Radio.Group>
|
||||
</FormSectionContent>
|
||||
</FormSection>
|
||||
<SidePanel.Separator />
|
||||
|
||||
<HTTPRequestFields
|
||||
type={values.function_type}
|
||||
errors={errors}
|
||||
httpHeaders={httpHeaders}
|
||||
httpParameters={httpParameters}
|
||||
onAddHeader={(header?: any) => {
|
||||
if (header) setHttpHeaders(httpHeaders.concat(header))
|
||||
else setHttpHeaders(httpHeaders.concat({ id: uuidv4(), name: '', value: '' }))
|
||||
}}
|
||||
onUpdateHeader={(idx, property, value) =>
|
||||
setHttpHeaders(
|
||||
httpHeaders.map((header, i) => {
|
||||
if (idx === i) return { ...header, [property]: value }
|
||||
else return header
|
||||
})
|
||||
)
|
||||
}
|
||||
onRemoveHeader={(idx) => setHttpHeaders(httpHeaders.filter((_, i) => idx !== i))}
|
||||
onAddParameter={() =>
|
||||
setHttpParameters(httpParameters.concat({ id: uuidv4(), name: '', value: '' }))
|
||||
}
|
||||
onUpdateParameter={(idx, property, value) =>
|
||||
setHttpParameters(
|
||||
httpParameters.map((param, i) => {
|
||||
if (idx === i) return { ...param, [property]: value }
|
||||
else return param
|
||||
})
|
||||
)
|
||||
}
|
||||
onRemoveParameter={(idx) => setHttpParameters(httpParameters.filter((_, i) => idx !== i))}
|
||||
/>
|
||||
|
||||
<button ref={submitRef} type="submit" className="hidden" />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
import { BASE_PATH } from 'lib/constants'
|
||||
import { BASE_PATH, IS_PLATFORM } from 'lib/constants'
|
||||
|
||||
export const ENABLED_MODE_OPTIONS = [
|
||||
{ label: 'Origin', value: 'ORIGIN', description: 'This is the default behaviour' },
|
||||
@@ -40,10 +40,14 @@ export const AVAILABLE_WEBHOOK_TYPES = [
|
||||
label: 'HTTP Request',
|
||||
description: 'Send an HTTP request to any URL.',
|
||||
},
|
||||
{
|
||||
value: 'supabase_function',
|
||||
icon: `${BASE_PATH}/img/function-providers/supabase-severless-function.png`,
|
||||
label: 'Supabase Edge Functions',
|
||||
description: 'Choose a Supabase edge function to run.',
|
||||
},
|
||||
...(IS_PLATFORM
|
||||
? [
|
||||
{
|
||||
value: 'supabase_function',
|
||||
icon: `${BASE_PATH}/img/function-providers/supabase-severless-function.png`,
|
||||
label: 'Supabase Edge Functions',
|
||||
description: 'Choose a Supabase edge function to run.',
|
||||
},
|
||||
]
|
||||
: []),
|
||||
]
|
||||
@@ -93,7 +93,7 @@ const HooksList = ({
|
||||
/>
|
||||
<div className="flex items-center space-x-2">
|
||||
<Link href="https://supabase.com/docs/guides/database/webhooks">
|
||||
<a target="_blank">
|
||||
<a target="_blank" rel="noreferrer">
|
||||
<Button type="default" icon={<IconExternalLink strokeWidth={1.5} />}>
|
||||
Documentation
|
||||
</Button>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { FC, createContext, useContext, useState } from 'react'
|
||||
import { createContext, useContext, useState } from 'react'
|
||||
import { useRouter } from 'next/router'
|
||||
import { observer, useLocalObservable } from 'mobx-react-lite'
|
||||
import { uniqBy, map as lodashMap } from 'lodash'
|
||||
@@ -24,19 +24,19 @@ class ChooseFunctionFormStore implements IChooseFunctionFormStore {
|
||||
|
||||
const ChooseFunctionFormContext = createContext<IChooseFunctionFormStore | null>(null)
|
||||
|
||||
type ChooseFunctionFormProps = {
|
||||
export interface ChooseFunctionFormProps {
|
||||
triggerFunctions: Dictionary<any>[]
|
||||
visible: boolean
|
||||
onChange: (id: number) => void
|
||||
setVisible: (value: boolean) => void
|
||||
}
|
||||
|
||||
const ChooseFunctionForm: FC<ChooseFunctionFormProps> = ({
|
||||
const ChooseFunctionForm = ({
|
||||
triggerFunctions,
|
||||
visible,
|
||||
onChange,
|
||||
setVisible,
|
||||
}) => {
|
||||
}: ChooseFunctionFormProps) => {
|
||||
const _localState = useLocalObservable(() => new ChooseFunctionFormStore())
|
||||
_localState.triggerFunctions = triggerFunctions as any
|
||||
|
||||
@@ -55,25 +55,27 @@ const ChooseFunctionForm: FC<ChooseFunctionFormProps> = ({
|
||||
onCancel={() => setVisible(!visible)}
|
||||
className="hooks-sidepanel"
|
||||
>
|
||||
{hasPublicSchemaFunctions ? (
|
||||
<ChooseFunctionFormContext.Provider value={_localState}>
|
||||
<div className="space-y-6">
|
||||
<NoticeBox />
|
||||
{_localState.functionSchemas.map((schema: string) => (
|
||||
<SchemaFunctionGroup key={schema} schema={schema} selectFunction={selectFunction} />
|
||||
))}
|
||||
</div>
|
||||
</ChooseFunctionFormContext.Provider>
|
||||
) : (
|
||||
<NoFunctionsState />
|
||||
)}
|
||||
<div className="py-6">
|
||||
{hasPublicSchemaFunctions ? (
|
||||
<ChooseFunctionFormContext.Provider value={_localState}>
|
||||
<div className="space-y-6">
|
||||
<NoticeBox />
|
||||
{_localState.functionSchemas.map((schema: string) => (
|
||||
<SchemaFunctionGroup key={schema} schema={schema} selectFunction={selectFunction} />
|
||||
))}
|
||||
</div>
|
||||
</ChooseFunctionFormContext.Provider>
|
||||
) : (
|
||||
<NoFunctionsState />
|
||||
)}
|
||||
</div>
|
||||
</SidePanel>
|
||||
)
|
||||
}
|
||||
|
||||
export default observer(ChooseFunctionForm)
|
||||
|
||||
const NoticeBox: FC = ({}) => {
|
||||
const NoticeBox = ({}) => {
|
||||
const router = useRouter()
|
||||
const { ref } = router.query
|
||||
return (
|
||||
@@ -84,7 +86,7 @@ const NoticeBox: FC = ({}) => {
|
||||
description={`You can make functions by using the Database Functions`}
|
||||
button={
|
||||
<Button
|
||||
type="secondary"
|
||||
type="default"
|
||||
onClick={() => {
|
||||
router.push(`/project/${ref}/database/functions`)
|
||||
}}
|
||||
@@ -97,7 +99,7 @@ const NoticeBox: FC = ({}) => {
|
||||
)
|
||||
}
|
||||
|
||||
const NoFunctionsState: FC = ({}) => {
|
||||
const NoFunctionsState = ({}) => {
|
||||
// for the empty 'no tables' state link
|
||||
const router = useRouter()
|
||||
const { ref } = router.query
|
||||
@@ -117,12 +119,12 @@ const NoFunctionsState: FC = ({}) => {
|
||||
)
|
||||
}
|
||||
|
||||
type SchemaFunctionGroupProps = {
|
||||
export interface SchemaFunctionGroupProps {
|
||||
schema: string
|
||||
selectFunction: (id: number) => void
|
||||
}
|
||||
|
||||
const SchemaFunctionGroup: FC<SchemaFunctionGroupProps> = observer(({ schema, selectFunction }) => {
|
||||
const SchemaFunctionGroup = observer(({ schema, selectFunction }: SchemaFunctionGroupProps) => {
|
||||
const _pageState = useContext(ChooseFunctionFormContext)
|
||||
const _functions = _pageState!.triggerFunctions.filter((x) => x.schema == schema)
|
||||
return (
|
||||
@@ -146,14 +148,14 @@ const SchemaFunctionGroup: FC<SchemaFunctionGroupProps> = observer(({ schema, se
|
||||
)
|
||||
})
|
||||
|
||||
type FunctionProps = {
|
||||
export interface FunctionProps {
|
||||
id: number
|
||||
completeStatement: string
|
||||
name: string
|
||||
onClick: (id: number) => void
|
||||
}
|
||||
|
||||
const Function: FC<FunctionProps> = ({ id, completeStatement, name, onClick }) => {
|
||||
const Function = ({ id, completeStatement, name, onClick }: FunctionProps) => {
|
||||
const [visible, setVisible] = useState(false)
|
||||
return (
|
||||
<div
|
||||
@@ -165,7 +167,7 @@ const Function: FC<FunctionProps> = ({ id, completeStatement, name, onClick }) =
|
||||
<div className="flex items-center justify-center rounded bg-scale-1200 p-1 text-scale-100 ">
|
||||
<IconTerminal strokeWidth={2} size={14} />
|
||||
</div>
|
||||
<h5 className="mb-0">{name}</h5>
|
||||
<p className="mb-0 text-sm">{name}</p>
|
||||
</div>
|
||||
<Button
|
||||
type="text"
|
||||
@@ -174,7 +176,7 @@ const Function: FC<FunctionProps> = ({ id, completeStatement, name, onClick }) =
|
||||
setVisible(!visible)
|
||||
}}
|
||||
icon={
|
||||
<IconChevronDown className={visible ? 'rotate-0 transform' : 'rotate-180 transform'} />
|
||||
<IconChevronDown className={visible ? 'rotate-180 transform' : 'rotate-0 transform'} />
|
||||
}
|
||||
>
|
||||
{visible ? 'Hide definition' : 'View definition'}
|
||||
|
||||
@@ -24,10 +24,10 @@ const SchemaTable: FC<Props> = ({ filterString, schema, editTrigger, deleteTrigg
|
||||
className="table-fixed px-6"
|
||||
head={
|
||||
<>
|
||||
<Table.th key="name" className="space-x-4">
|
||||
<Table.th key="name" className="w-[25%] space-x-4">
|
||||
Name
|
||||
</Table.th>
|
||||
<Table.th key="table" className="hidden lg:table-cell">
|
||||
<Table.th key="table" className="hidden w-[13%] lg:table-cell">
|
||||
Table
|
||||
</Table.th>
|
||||
<Table.th key="function" className="hidden xl:table-cell">
|
||||
|
||||
@@ -37,16 +37,16 @@ const TriggerList: FC<Props> = ({ filterString, schema, editTrigger, deleteTrigg
|
||||
<>
|
||||
{_triggers.map((x: any) => (
|
||||
<Table.tr key={x.id}>
|
||||
<Table.td className="space-x-2 break-all">
|
||||
<p>{x.name}</p>
|
||||
<Table.td className="space-x-2">
|
||||
<p title={x.name} className="truncate">
|
||||
{x.name}
|
||||
</p>
|
||||
</Table.td>
|
||||
<Table.td className="hidden lg:table-cell break-all">
|
||||
<p>{x.table}</p>
|
||||
<p title={x.table}>{x.table}</p>
|
||||
</Table.td>
|
||||
<Table.td className="hidden space-x-2 xl:table-cell">
|
||||
<div className="flex flex-col">
|
||||
<p>{x.function_name}</p>
|
||||
</div>
|
||||
<p title={x.function_name}>{x.function_name}</p>
|
||||
</Table.td>
|
||||
<Table.td className="hidden xl:table-cell">
|
||||
<div className="flex space-x-2">
|
||||
|
||||
@@ -143,7 +143,7 @@ const CreateWrapper = () => {
|
||||
<h3 className="mb-2 text-xl text-scale-1200">Create a {wrapperMeta?.label} Wrapper</h3>
|
||||
<div className="flex items-center space-x-2">
|
||||
<Link href="https://supabase.github.io/wrappers/stripe/">
|
||||
<a target="_blank">
|
||||
<a target="_blank" rel="noreferrer">
|
||||
<Button type="default" icon={<IconExternalLink strokeWidth={1.5} />}>
|
||||
Documentation
|
||||
</Button>
|
||||
@@ -229,7 +229,10 @@ const CreateWrapper = () => {
|
||||
) : (
|
||||
<div className="space-y-2">
|
||||
{newTables.map((table, i) => (
|
||||
<div className="flex items-center justify-between px-4 py-2 border rounded-md border-scale-600">
|
||||
<div
|
||||
key={`${table.schema_name}.${table.table_name}`}
|
||||
className="flex items-center justify-between px-4 py-2 border rounded-md border-scale-600"
|
||||
>
|
||||
<div>
|
||||
<p className="text-sm">
|
||||
{table.schema_name}.{table.table_name}
|
||||
|
||||