mirror of
https://github.com/supabase/supabase.git
synced 2026-10-17 23:29:08 +03:00
Merge branch 'feat/inline-editor-v2' of https://github.com/supabase/supabase into feat/inline-editor-v2
This commit is contained in:
commit
9cf514d307
55 files changed
+1235
-720
No files matched your search
@@ -1,9 +1,22 @@
|
||||
---
|
||||
id: 'json'
|
||||
title: 'Debugging and monitoring'
|
||||
description: 'Inspecting your Postgres database for common issues around disk, query performance, index, locks, and more using the terminal.'
|
||||
---
|
||||
|
||||
Database performance is a large topic and many factors can contribute. Some of the most common causes of poor performance include:
|
||||
|
||||
- An inefficiently designed schema
|
||||
- Inefficiently designed queries
|
||||
- A lack of indexes causing slower than required queries over large tables
|
||||
- Unused indexes causing slow `INSERT`, `UPDATE` and `DELETE` operations
|
||||
- Not enough compute resources, such as memory, causing your database to go to disk for results too often
|
||||
- Lock contention from multiple queries operating on highly utilized tables
|
||||
- Large amount of bloat on your tables causing poor query planning
|
||||
|
||||
You can examine your database and queries for these issues using either the [Supabase CLI](/docs/guides/local-development/cli/getting-started) or SQL.
|
||||
|
||||
## Using the CLI
|
||||
|
||||
The Supabase CLI comes with a range of tools to help inspect your Postgres instances for potential issues. The CLI gets the information from <a href="https://www.postgresql.org/docs/current/internals.html" target="_blank">Postgres internals</a>. Therefore, most tools provided are compatible with any Postgres databases regardless if they are a Supabase project or not.
|
||||
|
||||
You can find installation instructions for the the Supabase CLI <a href="/docs/guides/cli" target="_blank">here</a>.
|
||||
@@ -47,7 +60,7 @@ supabase link --project-ref <project-id>
|
||||
|
||||
Then the CLI will automatically connect to your Supabase project whenever you are in the project folder and you no longer need to provide `—db-url`.
|
||||
|
||||
## Inspection commands
|
||||
### Inspection commands
|
||||
|
||||
Below are the `db` inspection commands provided, grouped by different use cases.
|
||||
|
||||
@@ -57,7 +70,7 @@ Some commands might require `pg_stat_statements` to be enabled or a specific Pos
|
||||
|
||||
</Admonition>
|
||||
|
||||
### Disk storage
|
||||
#### Disk storage
|
||||
|
||||
These commands are handy if you are running low on disk storage:
|
||||
|
||||
@@ -68,7 +81,7 @@ These commands are handy if you are running low on disk storage:
|
||||
- [index-sizes](/docs/reference/cli/supabase-inspect-db-index-sizes) - shows the sizes of individual index
|
||||
- [table-index-sizes](/docs/reference/cli/supabase-inspect-db-table-index-sizes) - shows the sizes of indexes for each table
|
||||
|
||||
### Query performance
|
||||
#### Query performance
|
||||
|
||||
The commands below are useful if your Postgres database consumes a lot of resources like CPU, RAM or Disk IO. You can also use them to investigate slow queries.
|
||||
|
||||
@@ -79,17 +92,17 @@ The commands below are useful if your Postgres database consumes a lot of resour
|
||||
- [long-running-queries](/docs/reference/cli/supabase-inspect-db-long-running-queries) - shows long running queries that are executing right now
|
||||
- [outliers](/docs/reference/cli/supabase-inspect-db-outliers) - shows queries with high execution time but low call count and queries with high proportion of execution time spent on synchronous I/O
|
||||
|
||||
### Locks
|
||||
#### Locks
|
||||
|
||||
- [locks](/docs/reference/cli/supabase-inspect-db-locks) - shows statements which have taken out an exclusive lock on a relation
|
||||
- [blocking](/docs/reference/cli/supabase-inspect-db-blocking) - shows statements that are waiting for locks to be released
|
||||
|
||||
### Connections
|
||||
#### Connections
|
||||
|
||||
- [role-connections](/docs/reference/cli/supabase-inspect-db-role-connections) - shows number of active connections for all database roles (Supabase-specific command)
|
||||
- [replication-slots](/docs/reference/cli/supabase-inspect-db-replication-slots) - shows information about replication slots on the database
|
||||
|
||||
## Notes on `pg_stat_statements`
|
||||
### Notes on `pg_stat_statements`
|
||||
|
||||
Following commands require `pg_stat_statements` to be enabled: calls, locks, cache-hit, blocking, unused-indexes, index-usage, bloat, outliers, table-record-counts, replication-slots, seq-scans, vacuum-stats, long-running-queries.
|
||||
|
||||
@@ -97,6 +110,155 @@ When using `pg_stat_statements` also take note that it only stores the latest 5,
|
||||
|
||||
Learn more about pg_stats [here](https://supabase.com/docs/guides/database/extensions/pg_stat_statements).
|
||||
|
||||
## Acknowledgements
|
||||
## Using SQL
|
||||
|
||||
Supabase CLI's inspect commands are heavily inspired by the <a href="https://github.com/heroku/heroku-pg-extras" target="_blank">`pg-extras`</a> tools.
|
||||
<Admonition type="note">
|
||||
|
||||
If you're seeing an `insufficient privilege` error when viewing the Query Performance page from the dashboard, run this command:
|
||||
|
||||
```shell
|
||||
$ grant pg_read_all_stats to postgres;
|
||||
```
|
||||
|
||||
</Admonition>
|
||||
|
||||
### Postgres cumulative statistics system
|
||||
|
||||
Postgres collects data about its own operations using the [cumulative statistics system](https://www.postgresql.org/docs/current/monitoring-stats.html). In addition to this, every Supabase project has the [pg_stat_statements extension](/docs/guides/database/extensions/pg_stat_statements) enabled by default. This extension records query execution performance details and is the best way to find inefficient queries. This information can be combined with the Postgres query plan analyzer to develop more efficient queries.
|
||||
|
||||
Here are some example queries to get you started.
|
||||
|
||||
### Most frequently called queries
|
||||
|
||||
```sql
|
||||
select
|
||||
auth.rolname,
|
||||
statements.query,
|
||||
statements.calls,
|
||||
-- -- Postgres 13, 14, 15
|
||||
statements.total_exec_time + statements.total_plan_time as total_time,
|
||||
statements.min_exec_time + statements.min_plan_time as min_time,
|
||||
statements.max_exec_time + statements.max_plan_time as max_time,
|
||||
statements.mean_exec_time + statements.mean_plan_time as mean_time,
|
||||
-- -- Postgres <= 12
|
||||
-- total_time,
|
||||
-- min_time,
|
||||
-- max_time,
|
||||
-- mean_time,
|
||||
statements.rows / statements.calls as avg_rows
|
||||
from
|
||||
pg_stat_statements as statements
|
||||
inner join pg_authid as auth on statements.userid = auth.oid
|
||||
order by statements.calls desc
|
||||
limit 100;
|
||||
```
|
||||
|
||||
This query shows:
|
||||
|
||||
- query statistics, ordered by the number of times each query has been executed
|
||||
- the role that ran the query
|
||||
- the number of times it has been called
|
||||
- the average number of rows returned
|
||||
- the cumulative total time the query has spent running
|
||||
- the min, max and mean query times.
|
||||
|
||||
This provides useful information about the queries you run most frequently. Queries that have high `max_time` or `mean_time` times and are being called often can be good candidates for optimization.
|
||||
|
||||
### Slowest queries by execution time
|
||||
|
||||
```sql
|
||||
select
|
||||
auth.rolname,
|
||||
statements.query,
|
||||
statements.calls,
|
||||
-- -- Postgres 13, 14, 15
|
||||
statements.total_exec_time + statements.total_plan_time as total_time,
|
||||
statements.min_exec_time + statements.min_plan_time as min_time,
|
||||
statements.max_exec_time + statements.max_plan_time as max_time,
|
||||
statements.mean_exec_time + statements.mean_plan_time as mean_time,
|
||||
-- -- Postgres <= 12
|
||||
-- total_time,
|
||||
-- min_time,
|
||||
-- max_time,
|
||||
-- mean_time,
|
||||
statements.rows / statements.calls as avg_rows
|
||||
from
|
||||
pg_stat_statements as statements
|
||||
inner join pg_authid as auth on statements.userid = auth.oid
|
||||
order by max_time desc
|
||||
limit 100;
|
||||
```
|
||||
|
||||
This query will show you statistics about queries ordered by the maximum execution time. It is similar to the query above ordered by calls, but this one highlights outliers that may have high executions times. Queries which have high or mean execution times are good candidates for optimization.
|
||||
|
||||
### Most time consuming queries
|
||||
|
||||
```sql
|
||||
select
|
||||
auth.rolname,
|
||||
statements.query,
|
||||
statements.calls,
|
||||
statements.total_exec_time + statements.total_plan_time as total_time,
|
||||
to_char(
|
||||
(
|
||||
(statements.total_exec_time + statements.total_plan_time) / sum(
|
||||
statements.total_exec_time + statements.total_plan_time
|
||||
) over ()
|
||||
) * 100,
|
||||
'FM90D0'
|
||||
) || '%' as prop_total_time
|
||||
from
|
||||
pg_stat_statements as statements
|
||||
inner join pg_authid as auth on statements.userid = auth.oid
|
||||
order by total_time desc
|
||||
limit 100;
|
||||
```
|
||||
|
||||
This query will show you statistics about queries ordered by the cumulative total execution time. It shows the total time the query has spent running as well as the proportion of total execution time the query has taken up.
|
||||
|
||||
Queries which are the most time consuming are not necessarily bad, you may have a very efficient and frequently ran queries that end up taking a large total % time, but it can be useful to help spot queries that are taking up more time than they should.
|
||||
|
||||
### Hit rate
|
||||
|
||||
Generally for most applications a small percentage of data is accessed more regularly than the rest. To make sure that your regularly accessed data is available, Postgres tracks your data access patterns and keeps this in its [shared_buffers](https://www.postgresql.org/docs/15/runtime-config-resource.html#RUNTIME-CONFIG-RESOURCE-MEMORY) cache.
|
||||
|
||||
Applications with lower cache hit rates generally perform more poorly since they have to hit the disk to get results rather than serving them from memory. Very poor hit rates can also cause you to burst past your [Disk IO limits](./compute-add-ons#disk-io) causing significant performance issues.
|
||||
|
||||
You can view your cache and index hit rate by executing the following query:
|
||||
|
||||
```sql
|
||||
select
|
||||
'index hit rate' as name,
|
||||
(sum(idx_blks_hit)) / nullif(sum(idx_blks_hit + idx_blks_read), 0) * 100 as ratio
|
||||
from pg_statio_user_indexes
|
||||
union all
|
||||
select
|
||||
'table hit rate' as name,
|
||||
sum(heap_blks_hit) / nullif(sum(heap_blks_hit) + sum(heap_blks_read), 0) * 100 as ratio
|
||||
from pg_statio_user_tables;
|
||||
```
|
||||
|
||||
This shows the ratio of data blocks fetched from the Postgres [shared_buffers](https://www.postgresql.org/docs/15/runtime-config-resource.html#RUNTIME-CONFIG-RESOURCE-MEMORY) cache against the data blocks that were read from disk/OS cache.
|
||||
|
||||
If either of your index or table hit rate are < 99% then this can indicate your compute plan is too small for your current workload and you would benefit from more memory. [Upgrading your compute](./compute-add-ons) is easy and can be done from your [project dashboard](https://supabase.com/dashboard/project/_/settings/compute-and-disk).
|
||||
|
||||
### Optimizing poor performing queries
|
||||
|
||||
Postgres has built in tooling to help you optimize poorly performing queries. You can use the [query plan analyzer](https://www.postgresql.org/docs/current/sql-explain.html) on any expensive queries that you have identified:
|
||||
|
||||
```sql
|
||||
explain analyze <query-statement-here>;
|
||||
```
|
||||
|
||||
When you include `analyze` in the explain statement, the database attempts to execute the query and provides a detailed query plan along with actual execution times. So, be careful using `explain analyze` with `insert`/`update`/`delete` queries, because the query will actually run, and could have unintended side-effects.
|
||||
|
||||
If you run just `explain` without the `analyze` keyword, the database will only perform query planning without actually executing the query. This approach can be beneficial when you want to inspect the query plan without affecting the database or if you encounter timeouts in your queries.
|
||||
|
||||
Using the query plan analyzer to optimize your queries is a large topic, with a number of online resources available:
|
||||
|
||||
- [Official docs.](https://www.postgresql.org/docs/current/using-explain.html)
|
||||
- [The Art of PostgreSQL.](https://theartofpostgresql.com/explain-plan-visualizer/)
|
||||
- [Postgres Wiki.](https://wiki.postgresql.org/wiki/Using_EXPLAIN)
|
||||
- [Enterprise DB.](https://www.enterprisedb.com/blog/postgresql-query-optimization-performance-tuning-with-explain-analyze)
|
||||
|
||||
You can pair the information available from `pg_stat_statements` with the detailed system metrics available [via your metrics endpoint](../platform/metrics) to better understand the behavior of your DB and the queries you're executing against it.
|
||||
@@ -9,16 +9,25 @@ When you need granular authorization rules, nothing beats Postgres's [Row Level
|
||||
|
||||
## Row Level Security in Supabase
|
||||
|
||||
<Admonition type="danger">
|
||||
|
||||
Supabase allows convenient and secure data access from the browser, as long as you enable RLS.
|
||||
|
||||
RLS _must_ always be enabled on any tables stored in an exposed schema. By default, this is the `public` schema.
|
||||
|
||||
RLS is enabled by default on tables created with the Table Editor in the dashboard. If you create one in raw SQL or with the SQL editor, remember to enable RLS yourself:
|
||||
|
||||
```sql
|
||||
alter table <schema_name>.<table_name>
|
||||
enable row level security;
|
||||
```
|
||||
|
||||
</Admonition>
|
||||
|
||||
RLS is incredibly powerful and flexible, allowing you to write complex SQL rules that fit your unique business needs. RLS can be [combined with Supabase Auth](/docs/guides/database/postgres/row-level-security) for end-to-end user security from the browser to the database.
|
||||
|
||||
RLS is a Postgres primitive and can provide "[defense in depth](<https://en.wikipedia.org/wiki/Defense_in_depth_(computing)>)" to protect your data from malicious actors even when accessed through third-party tooling.
|
||||
|
||||
<Admonition type="note">
|
||||
|
||||
You should _always_ enable RLS on tables created in a public schema. This is done for you when you create a table with the Table Editor. If you create one in raw SQL or with the SQL Editor, remember to enable RLS yourself.
|
||||
|
||||
</Admonition>
|
||||
|
||||
## Policies
|
||||
|
||||
[Policies](https://www.postgresql.org/docs/current/sql-createpolicy.html) are Postgres's rule engine. Policies are easy to understand once you get the hang of them. Each policy is attached to a table, and the policy is executed every time a table is accessed.
|
||||
|
||||
@@ -136,7 +136,7 @@ Some entities like views and enums expect columns to be declared in a specific o
|
||||
<StepHikeCompact.Code>
|
||||
|
||||
```bash Terminal
|
||||
supabase diff -f add_age
|
||||
supabase db diff -f add_age
|
||||
```
|
||||
|
||||
</StepHikeCompact.Code>
|
||||
@@ -162,6 +162,23 @@ alter table "public"."employees" add column "age" smallint not null;
|
||||
</StepHikeCompact.Step>
|
||||
</StepHikeCompact>
|
||||
|
||||
<StepHikeCompact>
|
||||
<StepHikeCompact.Step step={5}>
|
||||
<StepHikeCompact.Details title="Apply the pending migration">
|
||||
Start the database locally and apply the pending migration.
|
||||
</StepHikeCompact.Details>
|
||||
|
||||
<StepHikeCompact.Code>
|
||||
|
||||
```bash Terminal
|
||||
supabase start && supabase migration up
|
||||
```
|
||||
|
||||
</StepHikeCompact.Code>
|
||||
|
||||
</StepHikeCompact.Step>
|
||||
</StepHikeCompact>
|
||||
|
||||
### Managing dependencies
|
||||
|
||||
As your database schema evolves, you will probably start using more advanced entities like views and functions. These entities are notoriously verbose to manage using plain migrations because the entire body must be recreated whenever there is a change. Using declarative schema, you can now edit them in-place so it’s much easier to review.
|
||||
|
||||
@@ -54,7 +54,7 @@ This example demonstrates setting up and testing RLS policies for a simple todo
|
||||
-- install tests utilities
|
||||
-- install pgtap extension for testing
|
||||
create extension if not exists pgtap with schema extensions;
|
||||
-- Start declare we'll have 6 test cases in our test suite
|
||||
-- Start declare we'll have 4 test cases in our test suite
|
||||
select plan(4);
|
||||
|
||||
-- Setup our testing data
|
||||
|
||||
@@ -35,7 +35,7 @@ Once a project satisfies at least one of the requirements for physical backups t
|
||||
|
||||
</Admonition>
|
||||
|
||||
You can confirm your project's backup type by navigating to [Database Backups > Scheduled backups](https://supabase.com/dashboard/project/_/backups/scheduled) and if you can download a backup then it is logical, otherwise it is physical.
|
||||
You can confirm your project's backup type by navigating to [Database Backups > Scheduled backups](https://supabase.com/dashboard/project/_/database/backups/scheduled) and if you can download a backup then it is logical, otherwise it is physical.
|
||||
|
||||
However, if your project has the Point-in-Time Recovery (PITR) add-on then the backups are physical and you can view them in [Database Backups > Point in time](https://supabase.com/dashboard/project/_/database/backups/pitr).
|
||||
|
||||
|
||||
@@ -8,170 +8,7 @@ The Supabase platform automatically optimizes your Postgres database to take adv
|
||||
|
||||
## Examining query performance
|
||||
|
||||
Unoptimized queries are a major cause of poor database performance. The techniques on this page can help you identify and understand queries that take the most time and resources from your database.
|
||||
|
||||
Database performance is a large topic and many factors can contribute. Some of the most common causes of poor performance include:
|
||||
|
||||
- An inefficiently designed schema
|
||||
- Inefficiently designed queries
|
||||
- A lack of indexes causing slower than required queries over large tables
|
||||
- Unused indexes causing slow `INSERT`, `UPDATE` and `DELETE` operations
|
||||
- Not enough compute resources, such as memory, causing your database to go to disk for results too often
|
||||
- Lock contention from multiple queries operating on highly utilized tables
|
||||
- Large amount of bloat on your tables causing poor query planning
|
||||
|
||||
Thankfully there are solutions to all these issues, which we will cover in the following sections.
|
||||
|
||||
<Admonition type="note">
|
||||
|
||||
If you're seeing an `insufficient privilege` error when viewing the Query Performance page from the dashboard, run this command:
|
||||
|
||||
```shell
|
||||
$ grant pg_read_all_stats to postgres;
|
||||
```
|
||||
|
||||
</Admonition>
|
||||
|
||||
### Postgres cumulative statistics system
|
||||
|
||||
Postgres collects data about its own operations using the [cumulative statistics system](https://www.postgresql.org/docs/current/monitoring-stats.html). In addition to this, every Supabase project has the [pg_stat_statements extension](/docs/guides/database/extensions/pg_stat_statements) enabled by default. This extension records query execution performance details and is the best way to find inefficient queries. This information can be combined with the Postgres query plan analyzer to develop more efficient queries.
|
||||
|
||||
Here are some example queries to get you started.
|
||||
|
||||
#### Most frequently called queries:
|
||||
|
||||
```sql
|
||||
select
|
||||
auth.rolname,
|
||||
statements.query,
|
||||
statements.calls,
|
||||
-- -- Postgres 13, 14, 15
|
||||
statements.total_exec_time + statements.total_plan_time as total_time,
|
||||
statements.min_exec_time + statements.min_plan_time as min_time,
|
||||
statements.max_exec_time + statements.max_plan_time as max_time,
|
||||
statements.mean_exec_time + statements.mean_plan_time as mean_time,
|
||||
-- -- Postgres <= 12
|
||||
-- total_time,
|
||||
-- min_time,
|
||||
-- max_time,
|
||||
-- mean_time,
|
||||
statements.rows / statements.calls as avg_rows
|
||||
from
|
||||
pg_stat_statements as statements
|
||||
inner join pg_authid as auth on statements.userid = auth.oid
|
||||
order by statements.calls desc
|
||||
limit 100;
|
||||
```
|
||||
|
||||
This query shows:
|
||||
|
||||
- query statistics, ordered by the number of times each query has been executed
|
||||
- the role that ran the query
|
||||
- the number of times it has been called
|
||||
- the average number of rows returned
|
||||
- the cumulative total time the query has spent running
|
||||
- the min, max and mean query times.
|
||||
|
||||
This provides useful information about the queries you run most frequently. Queries that have high `max_time` or `mean_time` times and are being called often can be good candidates for optimization.
|
||||
|
||||
#### Slowest queries by execution time:
|
||||
|
||||
```sql
|
||||
select
|
||||
auth.rolname,
|
||||
statements.query,
|
||||
statements.calls,
|
||||
-- -- Postgres 13, 14, 15
|
||||
statements.total_exec_time + statements.total_plan_time as total_time,
|
||||
statements.min_exec_time + statements.min_plan_time as min_time,
|
||||
statements.max_exec_time + statements.max_plan_time as max_time,
|
||||
statements.mean_exec_time + statements.mean_plan_time as mean_time,
|
||||
-- -- Postgres <= 12
|
||||
-- total_time,
|
||||
-- min_time,
|
||||
-- max_time,
|
||||
-- mean_time,
|
||||
statements.rows / statements.calls as avg_rows
|
||||
from
|
||||
pg_stat_statements as statements
|
||||
inner join pg_authid as auth on statements.userid = auth.oid
|
||||
order by max_time desc
|
||||
limit 100;
|
||||
```
|
||||
|
||||
This query will show you statistics about queries ordered by the maximum execution time. It is similar to the query above ordered by calls, but this one highlights outliers that may have high executions times. Queries which have high or mean execution times are good candidates for optimization.
|
||||
|
||||
#### Most time consuming queries:
|
||||
|
||||
```sql
|
||||
select
|
||||
auth.rolname,
|
||||
statements.query,
|
||||
statements.calls,
|
||||
statements.total_exec_time + statements.total_plan_time as total_time,
|
||||
to_char(
|
||||
(
|
||||
(statements.total_exec_time + statements.total_plan_time) / sum(
|
||||
statements.total_exec_time + statements.total_plan_time
|
||||
) over ()
|
||||
) * 100,
|
||||
'FM90D0'
|
||||
) || '%' as prop_total_time
|
||||
from
|
||||
pg_stat_statements as statements
|
||||
inner join pg_authid as auth on statements.userid = auth.oid
|
||||
order by total_time desc
|
||||
limit 100;
|
||||
```
|
||||
|
||||
This query will show you statistics about queries ordered by the cumulative total execution time. It shows the total time the query has spent running as well as the proportion of total execution time the query has taken up.
|
||||
|
||||
Queries which are the most time consuming are not necessarily bad, you may have a very efficient and frequently ran queries that end up taking a large total % time, but it can be useful to help spot queries that are taking up more time than they should.
|
||||
|
||||
### Hit rate
|
||||
|
||||
Generally for most applications a small percentage of data is accessed more regularly than the rest. To make sure that your regularly accessed data is available, Postgres tracks your data access patterns and keeps this in its [shared_buffers](https://www.postgresql.org/docs/15/runtime-config-resource.html#RUNTIME-CONFIG-RESOURCE-MEMORY) cache.
|
||||
|
||||
Applications with lower cache hit rates generally perform more poorly since they have to hit the disk to get results rather than serving them from memory. Very poor hit rates can also cause you to burst past your [Disk IO limits](./compute-add-ons#disk-io) causing significant performance issues.
|
||||
|
||||
You can view your cache and index hit rate by executing the following query:
|
||||
|
||||
```sql
|
||||
select
|
||||
'index hit rate' as name,
|
||||
(sum(idx_blks_hit)) / nullif(sum(idx_blks_hit + idx_blks_read), 0) * 100 as ratio
|
||||
from pg_statio_user_indexes
|
||||
union all
|
||||
select
|
||||
'table hit rate' as name,
|
||||
sum(heap_blks_hit) / nullif(sum(heap_blks_hit) + sum(heap_blks_read), 0) * 100 as ratio
|
||||
from pg_statio_user_tables;
|
||||
```
|
||||
|
||||
This shows the ratio of data blocks fetched from the Postgres [shared_buffers](https://www.postgresql.org/docs/15/runtime-config-resource.html#RUNTIME-CONFIG-RESOURCE-MEMORY) cache against the data blocks that were read from disk/OS cache.
|
||||
|
||||
If either of your index or table hit rate are < 99% then this can indicate your compute plan is too small for your current workload and you would benefit from more memory. [Upgrading your compute](./compute-add-ons) is easy and can be done from your [project dashboard](https://supabase.com/dashboard/project/_/settings/compute-and-disk).
|
||||
|
||||
### Optimizing poor performing queries
|
||||
|
||||
Postgres has built in tooling to help you optimize poorly performing queries. You can use the [query plan analyzer](https://www.postgresql.org/docs/current/sql-explain.html) on any expensive queries that you have identified:
|
||||
|
||||
```sql
|
||||
explain analyze <query-statement-here>;
|
||||
```
|
||||
|
||||
When you include `analyze` in the explain statement, the database attempts to execute the query and provides a detailed query plan along with actual execution times. So, be careful using `explain analyze` with `insert`/`update`/`delete` queries, because the query will actually run, and could have unintended side-effects.
|
||||
|
||||
If you run just `explain` without the `analyze` keyword, the database will only perform query planning without actually executing the query. This approach can be beneficial when you want to inspect the query plan without affecting the database or if you encounter timeouts in your queries.
|
||||
|
||||
Using the query plan analyzer to optimize your queries is a large topic, with a number of online resources available:
|
||||
|
||||
- [Official docs.](https://www.postgresql.org/docs/current/using-explain.html)
|
||||
- [The Art of PostgreSQL.](https://theartofpostgresql.com/explain-plan-visualizer/)
|
||||
- [Postgres Wiki.](https://wiki.postgresql.org/wiki/Using_EXPLAIN)
|
||||
- [Enterprise DB.](https://www.enterprisedb.com/blog/postgresql-query-optimization-performance-tuning-with-explain-analyze)
|
||||
|
||||
You can pair the information available from `pg_stat_statements` with the detailed system metrics available [via your metrics endpoint](../platform/metrics) to better understand the behavior of your DB and the queries you're executing against it.
|
||||
Unoptimized queries are a major cause of poor database performance. To analyze the performance of your queries, see the [Debugging and monitoring guide](/docs/guides/database/inspect).
|
||||
|
||||
## Optimizing the number of connections
|
||||
|
||||
|
||||
@@ -6,25 +6,8 @@ github_url = "https://github.com/orgs/supabase/discussions/33045"
|
||||
database_id = "91ae4c49-bf22-4b8c-b96b-cd11f1c38158"
|
||||
---
|
||||
|
||||
If you're unable to download backups due to physical backups being enabled, you can use the PostgreSQL utility `pg_dumpall` as an alternative. This tool offers more flexibility and control for creating backups.
|
||||
If you're unable to download backups due to physical backups being enabled, you can use the Supabase CLI command pgdump as an alternative.
|
||||
|
||||
You can use the command below to backup your database:
|
||||
`pg_dumpall -h your-db-host.supabase.co -U postgres > backup.sql`
|
||||
For step-by-step instructions, check out our guide: [Backup & Restore](https://supabase.com/docs/guides/platform/migrating-within-supabase/backup-restore).
|
||||
|
||||
Or alternatively, you can create a backup of your Supabase database and store it in your repository by running the following script: https://github.com/mansueli/Supa-Backup
|
||||
|
||||
To retrieve your database connection details from the dashboard:
|
||||
|
||||
1. Go to your Dashboard.
|
||||
2. Select your Project.
|
||||
3. Navigate to: https://supabase.com/dashboard/project/_?showConnect=true.
|
||||
4. Or click the "Connect" icon in the dashboard.
|
||||
5. There, you’ll see the connection strings that we have available.
|
||||
|
||||
<img
|
||||
width="106"
|
||||
alt="image"
|
||||
src="https://github.com/user-attachments/assets/7cdf38f4-f7d4-4b33-bc8e-a5dceedd7c34"
|
||||
/>
|
||||
|
||||
Additionally, please, refer to our documentation on [Database Backups](https://supabase.com/docs/guides/platform/backups).
|
||||
You can also learn more about how backups work in Supabase here: [Database Backups](https://supabase.com/docs/guides/platform/backups).
|
||||
@@ -37,6 +37,7 @@ Greg Richardson
|
||||
Guilherme Souza
|
||||
Haydn Maley
|
||||
Hieu Pham
|
||||
Illia Basalaiev
|
||||
Inian P
|
||||
Ivan Vasilov
|
||||
Jenny Kibiri
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { useProjectContext } from 'components/layouts/ProjectLayout/ProjectContext'
|
||||
import { useFlag } from 'hooks/ui/useFlag'
|
||||
import Link from 'next/link'
|
||||
import React from 'react'
|
||||
|
||||
@@ -10,7 +9,6 @@ type Props = {
|
||||
}
|
||||
|
||||
function DatabaseBackupsNav({ active }: Props) {
|
||||
const isCloneToNewProjectEnabled = useFlag('clonetonewproject')
|
||||
const { ref, cloud_provider } = useProjectContext()?.project || {}
|
||||
|
||||
const navMenuItems = [
|
||||
@@ -27,7 +25,7 @@ function DatabaseBackupsNav({ active }: Props) {
|
||||
href: `/project/${ref}/database/backups/pitr`,
|
||||
},
|
||||
{
|
||||
enabled: isCloneToNewProjectEnabled && cloud_provider !== 'FLY',
|
||||
enabled: cloud_provider !== 'FLY',
|
||||
id: 'rtnp',
|
||||
label: (
|
||||
<div className="flex items-center gap-1">
|
||||
|
||||
@@ -41,7 +41,8 @@ export const GraphiQLTab = () => {
|
||||
|
||||
const fetcher = useMemo(() => {
|
||||
const fetcherFn = createGraphiQLFetcher({
|
||||
url: `${API_URL}/projects/${projectRef}/api/graphql`,
|
||||
// [Joshen] Opting to hard code /platform for local to match the routes, so that it's clear what's happening
|
||||
url: `${API_URL}${IS_PLATFORM ? '' : '/platform'}/projects/${projectRef}/api/graphql`,
|
||||
fetch,
|
||||
})
|
||||
const customFetcher: Fetcher = async (graphqlParams, opts) => {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { zodResolver } from '@hookform/resolvers/zod'
|
||||
import { ExternalLink, TrashIcon } from 'lucide-react'
|
||||
import { TrashIcon } from 'lucide-react'
|
||||
import { ReactNode, useEffect, useState } from 'react'
|
||||
import { useForm } from 'react-hook-form'
|
||||
import { toast } from 'sonner'
|
||||
@@ -7,7 +7,7 @@ import { z } from 'zod'
|
||||
|
||||
import { useParams } from 'common'
|
||||
import { LogDrainData, useLogDrainsQuery } from 'data/log-drains/log-drains-query'
|
||||
import { useFlag } from 'hooks/ui/useFlag'
|
||||
|
||||
import {
|
||||
Button,
|
||||
Form_Shadcn_,
|
||||
@@ -136,7 +136,6 @@ export function LogDrainDestinationSheetForm({
|
||||
onSubmit: (values: z.infer<typeof formSchema>) => void
|
||||
mode: 'create' | 'update'
|
||||
}) {
|
||||
const lokiLogDrainsEnabled = useFlag('lokilogdrains')
|
||||
const CREATE_DEFAULT_HEADERS = {
|
||||
'Content-Type': 'application/json',
|
||||
}
|
||||
@@ -281,18 +280,16 @@ export function LogDrainDestinationSheetForm({
|
||||
{LOG_DRAIN_TYPES.find((t) => t.value === type)?.name}
|
||||
</SelectTrigger_Shadcn_>
|
||||
<SelectContent_Shadcn_>
|
||||
{LOG_DRAIN_TYPES.map((type) =>
|
||||
type.value === 'loki' && !lokiLogDrainsEnabled ? null : (
|
||||
<SelectItem_Shadcn_
|
||||
value={type.value}
|
||||
key={type.value}
|
||||
id={type.value}
|
||||
className="text-left"
|
||||
>
|
||||
{type.name}
|
||||
</SelectItem_Shadcn_>
|
||||
)
|
||||
)}
|
||||
{LOG_DRAIN_TYPES.map((type) => (
|
||||
<SelectItem_Shadcn_
|
||||
value={type.value}
|
||||
key={type.value}
|
||||
id={type.value}
|
||||
className="text-left"
|
||||
>
|
||||
{type.name}
|
||||
</SelectItem_Shadcn_>
|
||||
))}
|
||||
</SelectContent_Shadcn_>
|
||||
</Select_Shadcn_>
|
||||
</FormItemLayout>
|
||||
|
||||
@@ -27,7 +27,6 @@ import {
|
||||
import ConfirmationModal from 'ui-patterns/Dialogs/ConfirmationModal'
|
||||
import { GenericSkeletonLoader } from 'ui-patterns/ShimmeringLoader'
|
||||
import { LOG_DRAIN_TYPES, LogDrainType } from './LogDrains.constants'
|
||||
import { useFlag } from 'hooks/ui/useFlag'
|
||||
|
||||
export function LogDrains({
|
||||
onNewDrainClick,
|
||||
@@ -40,7 +39,6 @@ export function LogDrains({
|
||||
|
||||
const { isLoading: orgPlanLoading, plan } = useCurrentOrgPlan()
|
||||
const logDrainsEnabled = !orgPlanLoading && (plan?.id === 'team' || plan?.id === 'enterprise')
|
||||
const lokiLogDrainsEnabled = useFlag('lokilogdrains')
|
||||
|
||||
const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false)
|
||||
const [selectedLogDrain, setSelectedLogDrain] = useState<LogDrainData | null>(null)
|
||||
@@ -93,19 +91,17 @@ export function LogDrains({
|
||||
if (!isLoading && logDrains?.length === 0) {
|
||||
return (
|
||||
<div className="grid lg:grid-cols-2 gap-3">
|
||||
{LOG_DRAIN_TYPES.map((src) =>
|
||||
src.value === 'loki' && !lokiLogDrainsEnabled ? null : (
|
||||
<CardButton
|
||||
key={src.value}
|
||||
title={src.name}
|
||||
description={src.description}
|
||||
icon={src.icon}
|
||||
onClick={() => {
|
||||
onNewDrainClick(src.value)
|
||||
}}
|
||||
/>
|
||||
)
|
||||
)}
|
||||
{LOG_DRAIN_TYPES.map((src) => (
|
||||
<CardButton
|
||||
key={src.value}
|
||||
title={src.name}
|
||||
description={src.description}
|
||||
icon={src.icon}
|
||||
onClick={() => {
|
||||
onNewDrainClick(src.value)
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import Table from 'components/to-be-cleaned/Table'
|
||||
import type { AuthorizedApp } from 'data/oauth/authorized-apps-query'
|
||||
import dayjs from 'dayjs'
|
||||
import { Trash } from 'lucide-react'
|
||||
|
||||
import Table from 'components/to-be-cleaned/Table'
|
||||
import type { AuthorizedApp } from 'data/oauth/authorized-apps-query'
|
||||
import { Button } from 'ui'
|
||||
|
||||
export interface AuthorizedAppRowProps {
|
||||
|
||||
@@ -7,6 +7,7 @@ import Table from 'components/to-be-cleaned/Table'
|
||||
import type { OAuthApp } from 'data/oauth/oauth-apps-query'
|
||||
import { useCheckPermissions } from 'hooks/misc/useCheckPermissions'
|
||||
import { copyToClipboard } from 'lib/helpers'
|
||||
import { toast } from 'sonner'
|
||||
import {
|
||||
Button,
|
||||
DropdownMenu,
|
||||
@@ -56,11 +57,15 @@ const OAuthAppRow = ({ app, onSelectEdit, onSelectDelete }: OAuthAppRowProps) =>
|
||||
icon={isCopied ? <Check className="text-brand" strokeWidth={3} /> : <Clipboard />}
|
||||
className="ml-2 px-1"
|
||||
onClick={() => {
|
||||
copyToClipboard(app.client_id)
|
||||
setIsCopied(true)
|
||||
setTimeout(() => {
|
||||
setIsCopied(false)
|
||||
}, 3000)
|
||||
if (app.client_id) {
|
||||
copyToClipboard(app.client_id)
|
||||
setIsCopied(true)
|
||||
setTimeout(() => {
|
||||
setIsCopied(false)
|
||||
}, 3000)
|
||||
} else {
|
||||
toast('Unable to retrieve app client ID')
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { PermissionAction } from '@supabase/shared-types/out/constants'
|
||||
import { X } from 'lucide-react'
|
||||
import { useState } from 'react'
|
||||
|
||||
import { useParams } from 'common'
|
||||
@@ -18,7 +19,6 @@ import DeleteAppModal from './DeleteAppModal'
|
||||
import OAuthAppRow from './OAuthAppRow'
|
||||
import PublishAppModal from './PublishAppSidePanel'
|
||||
import RevokeAppModal from './RevokeAppModal'
|
||||
import { X } from 'lucide-react'
|
||||
|
||||
// [Joshen] Note on nav UX
|
||||
// Kang Ming mentioned that it might be better to split Published Apps and Authorized Apps into 2 separate tabs
|
||||
@@ -45,7 +45,7 @@ const OAuthApps = () => {
|
||||
} = useOAuthAppsQuery({ slug }, { enabled: canReadOAuthApps })
|
||||
|
||||
const sortedPublishedApps = publishedApps?.sort((a, b) => {
|
||||
return Number(new Date(a.created_at)) - Number(new Date(b.created_at))
|
||||
return Number(new Date(a.created_at ?? '')) - Number(new Date(b.created_at ?? ''))
|
||||
})
|
||||
|
||||
const {
|
||||
|
||||
+3
-3
@@ -85,11 +85,11 @@ const PublishAppSidePanel = ({
|
||||
|
||||
if (selectedApp !== undefined) {
|
||||
setUrls(
|
||||
selectedApp.redirect_uris.map((url) => {
|
||||
selectedApp.redirect_uris?.map((url) => {
|
||||
return { id: uuidv4(), value: url }
|
||||
})
|
||||
}) ?? []
|
||||
)
|
||||
setScopes(selectedApp.scopes)
|
||||
setScopes((selectedApp?.scopes ?? []) as OAuthScope[])
|
||||
setIconUrl(selectedApp.icon === null ? undefined : selectedApp.icon)
|
||||
} else {
|
||||
setUrls([{ id: uuidv4(), value: '' }])
|
||||
|
||||
+3
-3
@@ -15,9 +15,9 @@ const columns: Column<LogData>[] = [
|
||||
return (
|
||||
<RowLayout>
|
||||
<TimestampInfo utcTimestamp={props.row.timestamp!} />
|
||||
<ResponseCodeFormatter row={props} value={props.row.status_code} />
|
||||
<TextFormatter className="w-20" value={props.row.method as string} />
|
||||
<TextFormatter className="w-full" value={props.row.path as string} />
|
||||
<ResponseCodeFormatter value={String(props.row.status_code)} />
|
||||
<TextFormatter value={String(props.row.method)} />
|
||||
<TextFormatter value={String(props.row.path)} />
|
||||
</RowLayout>
|
||||
)
|
||||
},
|
||||
|
||||
+3
-3
@@ -15,9 +15,9 @@ const columns: Column<LogData>[] = [
|
||||
return (
|
||||
<RowLayout>
|
||||
<TimestampInfo utcTimestamp={props.row.timestamp!} />
|
||||
<ResponseCodeFormatter row={props} value={props.row.status_code} />
|
||||
<TextFormatter value={props.row.method as string} />
|
||||
<TextFormatter value={props.row.id} />
|
||||
<ResponseCodeFormatter value={String(props.row.status_code)} />
|
||||
<TextFormatter value={String(props.row.method)} />
|
||||
<TextFormatter value={String(props.row.id)} />
|
||||
</RowLayout>
|
||||
)
|
||||
},
|
||||
|
||||
@@ -8,17 +8,9 @@ import {
|
||||
Tabs_Shadcn_,
|
||||
cn,
|
||||
} from 'ui'
|
||||
import AuthSelectionRenderer from './LogSelectionRenderers/AuthSelectionRenderer'
|
||||
import DatabaseApiSelectionRender from './LogSelectionRenderers/DatabaseApiSelectionRender'
|
||||
import DatabasePostgresSelectionRender from './LogSelectionRenderers/DatabasePostgresSelectionRender'
|
||||
import DefaultPreviewSelectionRenderer from './LogSelectionRenderers/DefaultPreviewSelectionRenderer'
|
||||
import FunctionInvocationSelectionRender from './LogSelectionRenderers/FunctionInvocationSelectionRender'
|
||||
import FunctionLogsSelectionRender from './LogSelectionRenderers/FunctionLogsSelectionRender'
|
||||
import type { LogData, QueryType } from './Logs.types'
|
||||
import { GenericSkeletonLoader } from 'ui-patterns/ShimmeringLoader'
|
||||
import { WarehouseSelectionRenderer } from './LogSelectionRenderers/WarehouseSelectionRenderer'
|
||||
import { useFlag } from 'hooks/ui/useFlag'
|
||||
import { IS_PLATFORM } from 'lib/constants'
|
||||
|
||||
export interface LogSelectionProps {
|
||||
log?: LogData
|
||||
@@ -31,32 +23,10 @@ export interface LogSelectionProps {
|
||||
}
|
||||
|
||||
const LogSelection = ({ log, onClose, queryType, isLoading, error }: LogSelectionProps) => {
|
||||
const useNewLogDetail = useFlag('logsDetailV2')
|
||||
|
||||
const LogDetails = () => {
|
||||
if (error) return <LogErrorState error={error} />
|
||||
if (!log) return <LogDetailEmptyState />
|
||||
if (useNewLogDetail || IS_PLATFORM === false)
|
||||
return <DefaultPreviewSelectionRenderer log={log} />
|
||||
|
||||
switch (queryType) {
|
||||
case 'warehouse':
|
||||
return <WarehouseSelectionRenderer log={log} />
|
||||
case 'api':
|
||||
return <DatabaseApiSelectionRender log={log} />
|
||||
case 'database':
|
||||
return <DatabasePostgresSelectionRender log={log} />
|
||||
case 'pg_cron':
|
||||
return <DatabasePostgresSelectionRender log={log} />
|
||||
case 'fn_edge':
|
||||
return <FunctionInvocationSelectionRender log={log} />
|
||||
case 'functions':
|
||||
return <FunctionLogsSelectionRender log={log} />
|
||||
case 'auth':
|
||||
return <AuthSelectionRenderer log={log} />
|
||||
default:
|
||||
return <DefaultPreviewSelectionRenderer log={log} />
|
||||
}
|
||||
return <DefaultPreviewSelectionRenderer log={log} />
|
||||
}
|
||||
|
||||
return (
|
||||
|
||||
+17
-4
@@ -16,7 +16,15 @@ import { useState, useEffect } from 'react'
|
||||
|
||||
const LogRowSeparator = () => <Separator className="bg-border my-1" />
|
||||
|
||||
const PropertyRow = ({ keyName, value }: { keyName: string; value: any }) => {
|
||||
const PropertyRow = ({
|
||||
keyName,
|
||||
value,
|
||||
dataTestId,
|
||||
}: {
|
||||
keyName: string
|
||||
value: any
|
||||
dataTestId?: string
|
||||
}) => {
|
||||
const isTimestamp =
|
||||
keyName === 'timestamp' || keyName === 'created_at' || keyName === 'updated_at'
|
||||
|
||||
@@ -83,7 +91,7 @@ const PropertyRow = ({ keyName, value }: { keyName: string; value: any }) => {
|
||||
|
||||
return (
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger className="group w-full">
|
||||
<DropdownMenuTrigger className="group w-full" data-testid={dataTestId}>
|
||||
<div className="rounded-md w-full overflow-hidden">
|
||||
<div
|
||||
className={cn('flex py-2 w-full', {
|
||||
@@ -131,10 +139,15 @@ const DefaultPreviewSelectionRenderer = ({ log }: { log: PreviewLogData }) => {
|
||||
const { timestamp, event_message, metadata, id, ...rest } = log
|
||||
|
||||
return (
|
||||
<div className={`p-2 flex flex-col`}>
|
||||
<div data-testid="log-selection" className={`p-2 flex flex-col`}>
|
||||
{log?.id && <PropertyRow key={'id'} keyName={'id'} value={log.id} />}
|
||||
{log?.timestamp && (
|
||||
<PropertyRow key={'timestamp'} keyName={'timestamp'} value={log.timestamp} />
|
||||
<PropertyRow
|
||||
dataTestId="log-selection-timestamp"
|
||||
key={'timestamp'}
|
||||
keyName={'timestamp'}
|
||||
value={log.timestamp}
|
||||
/>
|
||||
)}
|
||||
|
||||
{log?.event_message && (
|
||||
|
||||
@@ -9,6 +9,7 @@ import React from 'react'
|
||||
import { isUnixMicro, unixMicroToIsoTimestamp } from './Logs.utils'
|
||||
import { AlertCircle, Info } from 'lucide-react'
|
||||
import dayjs from 'dayjs'
|
||||
import { cn } from 'ui'
|
||||
|
||||
export const RowLayout: React.FC<React.PropsWithChildren> = ({ children }) => (
|
||||
<div className="flex h-full w-full items-center gap-4">{children}</div>
|
||||
@@ -57,7 +58,7 @@ export const TextFormatter: React.FC<{ className?: string; value: string }> = ({
|
||||
className,
|
||||
}) => <span className={'font-mono text-xs truncate ' + className}>{value}</span>
|
||||
|
||||
export const ResponseCodeFormatter = ({ value }: any) => {
|
||||
export const ResponseCodeFormatter = ({ value }: { value: string }) => {
|
||||
if (!value) {
|
||||
return (
|
||||
<div>
|
||||
@@ -66,66 +67,49 @@ export const ResponseCodeFormatter = ({ value }: any) => {
|
||||
)
|
||||
}
|
||||
|
||||
const ResponseCodeItem = ({
|
||||
children,
|
||||
className,
|
||||
}: {
|
||||
children: React.ReactNode
|
||||
className?: string
|
||||
}) => (
|
||||
<div className="flex h-full items-center">
|
||||
<div
|
||||
className={cn(
|
||||
`relative flex h-6 items-center rounded-md justify-center px-2 py-1 text-center`,
|
||||
className
|
||||
)}
|
||||
>
|
||||
<label className="block font-mono text-sm">{children}</label>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
const split = value.toString().split('')[0]
|
||||
|
||||
switch (split) {
|
||||
// 2XX || 1XX responses
|
||||
case '1':
|
||||
return <ResponseCodeItem>{value}</ResponseCodeItem>
|
||||
case '2':
|
||||
return (
|
||||
<div className="flex h-full items-center">
|
||||
<div className="relative flex h-6 items-center justify-center rounded border bg-surface-200 px-2 py-1 text-center">
|
||||
<label className="block font-mono text-sm text-foreground-lighter">{value}</label>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
break
|
||||
return <ResponseCodeItem className="bg-surface-100 text-brand">{value}</ResponseCodeItem>
|
||||
// 5XX responses
|
||||
case '5':
|
||||
return (
|
||||
<div className="flex h-full items-center">
|
||||
<div
|
||||
className="relative flex h-6 items-center justify-center rounded bg-red-400 px-2 py-1
|
||||
text-center
|
||||
return <ResponseCodeItem className="bg-red-300 text-red-1100">{value}</ResponseCodeItem>
|
||||
|
||||
"
|
||||
>
|
||||
<label className="block font-mono text-sm text-red-1100">{value}</label>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
break
|
||||
// 4XX || 3XX responses
|
||||
case '4':
|
||||
case '3':
|
||||
return (
|
||||
<div className="flex h-full items-center">
|
||||
<div
|
||||
className="relative flex h-6 items-center justify-center rounded bg-amber-400 px-2 py-1
|
||||
text-center
|
||||
return <ResponseCodeItem className="bg-amber-300 text-amber-1100">{value}</ResponseCodeItem>
|
||||
|
||||
"
|
||||
>
|
||||
<label className="block font-mono text-sm text-amber-1100">{value}</label>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
break
|
||||
// All other responses
|
||||
default:
|
||||
return (
|
||||
<div className="flex h-full items-center">
|
||||
<div
|
||||
className="relative flex h-6 items-center justify-center rounded bg-surface-100 px-2 py-1
|
||||
text-center
|
||||
|
||||
"
|
||||
>
|
||||
<label className="block font-mono text-sm text-foreground-lighter">{value}</label>
|
||||
</div>
|
||||
</div>
|
||||
<ResponseCodeItem className="bg-surface-100 text-foreground-lighter">
|
||||
{value}
|
||||
</ResponseCodeItem>
|
||||
)
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@ import { ButtonTooltip } from 'components/ui/ButtonTooltip'
|
||||
import CSVButton from 'components/ui/CSVButton'
|
||||
import DatabaseSelector from 'components/ui/DatabaseSelector'
|
||||
import { useLoadBalancersQuery } from 'data/read-replicas/load-balancers-query'
|
||||
import { IS_PLATFORM } from 'lib/constants'
|
||||
import { Button, Input, Tooltip, TooltipContent, TooltipTrigger, cn } from 'ui'
|
||||
import DatePickers from './Logs.DatePickers'
|
||||
import {
|
||||
@@ -72,7 +73,8 @@ const PreviewFilterPanel = ({
|
||||
const { data: loadBalancers } = useLoadBalancersQuery({ projectRef: ref })
|
||||
|
||||
// [Joshen] These are the routes tested that can show replica logs
|
||||
const showDatabaseSelector = LOG_ROUTES_WITH_REPLICA_SUPPORT.includes(router.pathname)
|
||||
const showDatabaseSelector =
|
||||
IS_PLATFORM && LOG_ROUTES_WITH_REPLICA_SUPPORT.includes(router.pathname)
|
||||
|
||||
const hasEdits = search !== defaultSearchValue
|
||||
|
||||
|
||||
@@ -46,7 +46,7 @@ const FileExplorer = ({
|
||||
return (
|
||||
<div
|
||||
ref={fileExplorerRef}
|
||||
className="file-explorer flex flex-grow overflow-x-auto justify-between h-full w-full"
|
||||
className="file-explorer flex flex-grow overflow-x-auto justify-between h-full w-full relative"
|
||||
>
|
||||
<ColumnContextMenu id={CONTEXT_MENU_KEYS.STORAGE_COLUMN} />
|
||||
<ItemContextMenu id={CONTEXT_MENU_KEYS.STORAGE_ITEM} />
|
||||
|
||||
+109
-106
@@ -142,134 +142,137 @@ const FileExplorerColumn = ({
|
||||
)
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={fileExplorerColumnRef}
|
||||
className={`
|
||||
<>
|
||||
<div
|
||||
ref={fileExplorerColumnRef}
|
||||
className={`
|
||||
${fullWidth ? 'w-full' : 'w-64 border-r border-overlay'}
|
||||
${view === STORAGE_VIEWS.COLUMNS ? '' : ''}
|
||||
hide-scrollbar relative flex flex-shrink-0 flex-col overflow-auto
|
||||
`}
|
||||
onContextMenu={displayMenu}
|
||||
onDragOver={onDragOver}
|
||||
onDrop={onDrop}
|
||||
onClick={(event) => {
|
||||
const eventTarget = get(event.target, ['className'], '')
|
||||
if (typeof eventTarget === 'string' && eventTarget.includes('react-contexify')) return
|
||||
onSelectColumnEmptySpace(index)
|
||||
}}
|
||||
>
|
||||
{/* Checkbox selection for select all */}
|
||||
{view === STORAGE_VIEWS.COLUMNS && (
|
||||
<div
|
||||
className={`sticky top-0 z-10 mb-0 flex items-center bg-table-header-light px-2.5 [[data-theme*=dark]_&]:bg-table-header-dark ${
|
||||
haveSelectedItems ? 'h-10 py-3 opacity-100' : 'h-0 py-0 opacity-0'
|
||||
} transition-all duration-200`}
|
||||
onClick={(event) => event.stopPropagation()}
|
||||
>
|
||||
{columnFiles.length > 0 ? (
|
||||
<>
|
||||
<SelectAllCheckbox />
|
||||
<p className="text-sm text-foreground-light">Select all {columnFiles.length} files</p>
|
||||
</>
|
||||
) : (
|
||||
<p className="text-sm text-foreground-light">No files available for selection</p>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
onContextMenu={displayMenu}
|
||||
onDragOver={onDragOver}
|
||||
onDrop={onDrop}
|
||||
onClick={(event) => {
|
||||
const eventTarget = get(event.target, ['className'], '')
|
||||
if (typeof eventTarget === 'string' && eventTarget.includes('react-contexify')) return
|
||||
onSelectColumnEmptySpace(index)
|
||||
}}
|
||||
>
|
||||
{/* Checkbox selection for select all */}
|
||||
{view === STORAGE_VIEWS.COLUMNS && (
|
||||
<div
|
||||
className={`sticky top-0 z-10 mb-0 flex items-center bg-table-header-light px-2.5 [[data-theme*=dark]_&]:bg-table-header-dark ${
|
||||
haveSelectedItems ? 'h-10 py-3 opacity-100' : 'h-0 py-0 opacity-0'
|
||||
} transition-all duration-200`}
|
||||
onClick={(event) => event.stopPropagation()}
|
||||
>
|
||||
{columnFiles.length > 0 ? (
|
||||
<>
|
||||
<SelectAllCheckbox />
|
||||
<p className="text-sm text-foreground-light">
|
||||
Select all {columnFiles.length} files
|
||||
</p>
|
||||
</>
|
||||
) : (
|
||||
<p className="text-sm text-foreground-light">No files available for selection</p>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* List Interface Header */}
|
||||
{view === STORAGE_VIEWS.LIST && (
|
||||
<div
|
||||
className="
|
||||
{/* List Interface Header */}
|
||||
{view === STORAGE_VIEWS.LIST && (
|
||||
<div
|
||||
className="
|
||||
sticky top-0
|
||||
z-10 flex min-w-min items-center border-b border-overlay bg-surface-100 px-2.5
|
||||
py-2
|
||||
"
|
||||
>
|
||||
<div className="flex w-[40%] min-w-[250px] items-center">
|
||||
<SelectAllCheckbox />
|
||||
<p className="text-sm">Name</p>
|
||||
>
|
||||
<div className="flex w-[40%] min-w-[250px] items-center">
|
||||
<SelectAllCheckbox />
|
||||
<p className="text-sm">Name</p>
|
||||
</div>
|
||||
<p className="w-[11%] min-w-[100px] text-sm">Size</p>
|
||||
<p className="w-[14%] min-w-[100px] text-sm">Type</p>
|
||||
<p className="w-[15%] min-w-[160px] text-sm">Created at</p>
|
||||
<p className="w-[15%] min-w-[160px] text-sm">Last modified at</p>
|
||||
</div>
|
||||
<p className="w-[11%] min-w-[100px] text-sm">Size</p>
|
||||
<p className="w-[14%] min-w-[100px] text-sm">Type</p>
|
||||
<p className="w-[15%] min-w-[160px] text-sm">Created at</p>
|
||||
<p className="w-[15%] min-w-[160px] text-sm">Last modified at</p>
|
||||
</div>
|
||||
)}
|
||||
)}
|
||||
|
||||
{/* Shimmering loaders while fetching contents */}
|
||||
{column.status === STORAGE_ROW_STATUS.LOADING && (
|
||||
<div
|
||||
className={`
|
||||
{/* Shimmering loaders while fetching contents */}
|
||||
{column.status === STORAGE_ROW_STATUS.LOADING && (
|
||||
<div
|
||||
className={`
|
||||
${fullWidth ? 'w-full' : 'w-64 border-r border-default'}
|
||||
px-2 py-1 my-1 flex flex-shrink-0 flex-col space-y-2 overflow-auto
|
||||
`}
|
||||
>
|
||||
<ShimmeringLoader />
|
||||
<ShimmeringLoader />
|
||||
<ShimmeringLoader />
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Column Interface */}
|
||||
<InfiniteList
|
||||
items={columnItems}
|
||||
itemProps={{
|
||||
view,
|
||||
columnIndex: index,
|
||||
selectedItems,
|
||||
openedFolders,
|
||||
}}
|
||||
ItemComponent={FileExplorerRow}
|
||||
getItemSize={(index) => (index !== 0 && index === columnItems.length ? 85 : 37)}
|
||||
hasNextPage={column.status !== STORAGE_ROW_STATUS.LOADING && column.hasMoreItems}
|
||||
isLoadingNextPage={column.isLoadingMoreItems}
|
||||
onLoadNextPage={() => onColumnLoadMore(index, column)}
|
||||
/>
|
||||
|
||||
{/* Drag drop upload CTA for when column is empty */}
|
||||
{!(snap.isSearching && itemSearchString.length > 0) &&
|
||||
column.items.length === 0 &&
|
||||
column.status !== STORAGE_ROW_STATUS.LOADING && (
|
||||
<div className="h-full w-full flex flex-col items-center justify-center">
|
||||
<img
|
||||
alt="storage-placeholder"
|
||||
src={`${BASE_PATH}/img/storage-placeholder.svg`}
|
||||
className="opacity-75 pointer-events-none"
|
||||
/>
|
||||
<p className="text-sm my-3 opacity-75">Drop your files here</p>
|
||||
<p className="w-40 text-center text-xs text-foreground-light">
|
||||
Or upload them via the "Upload file" button above
|
||||
</p>
|
||||
>
|
||||
<ShimmeringLoader />
|
||||
<ShimmeringLoader />
|
||||
<ShimmeringLoader />
|
||||
</div>
|
||||
)}
|
||||
|
||||
{snap.isSearching &&
|
||||
itemSearchString.length > 0 &&
|
||||
isEmpty &&
|
||||
column.status !== STORAGE_ROW_STATUS.LOADING && (
|
||||
<div className="h-full w-full flex flex-col items-center justify-center">
|
||||
<p className="text-sm my-3 text-foreground">No results found in this folder</p>
|
||||
<p className="w-40 text-center text-sm text-foreground-light">
|
||||
Your search for "{itemSearchString}" did not return any results
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
{/* Column Interface */}
|
||||
<InfiniteList
|
||||
items={columnItems}
|
||||
itemProps={{
|
||||
view,
|
||||
columnIndex: index,
|
||||
selectedItems,
|
||||
openedFolders,
|
||||
}}
|
||||
ItemComponent={FileExplorerRow}
|
||||
getItemSize={(index) => (index !== 0 && index === columnItems.length ? 85 : 37)}
|
||||
hasNextPage={column.status !== STORAGE_ROW_STATUS.LOADING && column.hasMoreItems}
|
||||
isLoadingNextPage={column.isLoadingMoreItems}
|
||||
onLoadNextPage={() => onColumnLoadMore(index, column)}
|
||||
/>
|
||||
|
||||
{/* Drag drop upload CTA for when column has files */}
|
||||
<DragOverOverlay
|
||||
isOpen={isDraggedOver}
|
||||
folderIsEmpty={isEmpty}
|
||||
onDragLeave={() => setIsDraggedOver(false)}
|
||||
onDrop={() => setIsDraggedOver(false)}
|
||||
/>
|
||||
{/* Drag drop upload CTA for when column is empty */}
|
||||
{!(snap.isSearching && itemSearchString.length > 0) &&
|
||||
column.items.length === 0 &&
|
||||
column.status !== STORAGE_ROW_STATUS.LOADING && (
|
||||
<div className="h-full w-full flex flex-col items-center justify-center">
|
||||
<img
|
||||
alt="storage-placeholder"
|
||||
src={`${BASE_PATH}/img/storage-placeholder.svg`}
|
||||
className="opacity-75 pointer-events-none"
|
||||
/>
|
||||
<p className="text-sm my-3 opacity-75">Drop your files here</p>
|
||||
<p className="w-40 text-center text-xs text-foreground-light">
|
||||
Or upload them via the "Upload file" button above
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{snap.isSearching &&
|
||||
itemSearchString.length > 0 &&
|
||||
isEmpty &&
|
||||
column.status !== STORAGE_ROW_STATUS.LOADING && (
|
||||
<div className="h-full w-full flex flex-col items-center justify-center">
|
||||
<p className="text-sm my-3 text-foreground">No results found in this folder</p>
|
||||
<p className="w-40 text-center text-sm text-foreground-light">
|
||||
Your search for "{itemSearchString}" did not return any results
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Drag drop upload CTA for when column has files */}
|
||||
<DragOverOverlay
|
||||
isOpen={isDraggedOver}
|
||||
folderIsEmpty={isEmpty}
|
||||
onDragLeave={() => setIsDraggedOver(false)}
|
||||
onDrop={() => setIsDraggedOver(false)}
|
||||
/>
|
||||
</div>
|
||||
{/* List interface footer */}
|
||||
{view === STORAGE_VIEWS.LIST && (
|
||||
<div
|
||||
className="
|
||||
sticky bottom-0
|
||||
z-10 flex min-w-min items-center bg-panel-footer-light px-2.5 py-2 [[data-theme*=dark]_&]:bg-panel-footer-dark
|
||||
absolute bottom-0 rounded-b-md mt-auto
|
||||
z-10 flex min-w-min items-center bg-panel-footer-light px-2.5 py-2 [[data-theme*=dark]_&]:bg-panel-footer-dark w-full
|
||||
"
|
||||
>
|
||||
<p className="text-sm">
|
||||
@@ -277,7 +280,7 @@ const FileExplorerColumn = ({
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -151,7 +151,7 @@ export const MarkdownPre = ({ children }: { children: any }) => {
|
||||
|
||||
const language = children[0].props.className?.replace('language-', '') || 'sql'
|
||||
const rawContent = children[0].props.children[0]
|
||||
const propsMatch = rawContent.match(/--\s*props:\s*(\{[^}]+\})/)
|
||||
const propsMatch = rawContent.match(/(?:--|\/\/)\s*props:\s*(\{[^}]+\})/)
|
||||
|
||||
const snippetProps: AssistantSnippetProps = useMemo(
|
||||
() => (propsMatch ? JSON.parse(propsMatch[1]) : {}),
|
||||
@@ -164,7 +164,7 @@ export const MarkdownPre = ({ children }: { children: any }) => {
|
||||
const runQuery = snippetProps.runQuery === 'true'
|
||||
|
||||
// Strip props from the content for both SQL and edge functions
|
||||
const cleanContent = rawContent.replace(/--\s*props:\s*\{[^}]+\}/, '').trim()
|
||||
const cleanContent = rawContent.replace(/(?:--|\/\/)\s*props:\s*\{[^}]+\}/, '').trim()
|
||||
|
||||
const isDraggableToReports =
|
||||
supportSQLBlocks && canCreateSQLSnippet && router.pathname.endsWith('/reports/[id]')
|
||||
|
||||
@@ -111,33 +111,34 @@ function InfiniteList<T, P>({
|
||||
|
||||
return (
|
||||
<>
|
||||
<AutoSizer>
|
||||
{({ height, width }: { height: number; width: number }) => (
|
||||
<InfiniteLoader
|
||||
itemCount={itemCount}
|
||||
isItemLoaded={isItemLoaded}
|
||||
loadMoreItems={loadMoreItems}
|
||||
>
|
||||
{({ onItemsRendered, ref }) => (
|
||||
<VariableSizeList
|
||||
ref={(refy) => {
|
||||
ref(refy)
|
||||
listRef.current = refy
|
||||
}}
|
||||
height={height ?? 0}
|
||||
width={width ?? 0}
|
||||
itemCount={itemCount}
|
||||
itemData={itemData}
|
||||
itemSize={getItemSize}
|
||||
onItemsRendered={onItemsRendered}
|
||||
>
|
||||
{Item}
|
||||
</VariableSizeList>
|
||||
)}
|
||||
</InfiniteLoader>
|
||||
)}
|
||||
</AutoSizer>
|
||||
|
||||
<div className="flex-grow">
|
||||
<AutoSizer>
|
||||
{({ height, width }: { height: number; width: number }) => (
|
||||
<InfiniteLoader
|
||||
itemCount={itemCount}
|
||||
isItemLoaded={isItemLoaded}
|
||||
loadMoreItems={loadMoreItems}
|
||||
>
|
||||
{({ onItemsRendered, ref }) => (
|
||||
<VariableSizeList
|
||||
ref={(refy) => {
|
||||
ref(refy)
|
||||
listRef.current = refy
|
||||
}}
|
||||
height={height ?? 0}
|
||||
width={width ?? 0}
|
||||
itemCount={itemCount}
|
||||
itemData={itemData}
|
||||
itemSize={getItemSize}
|
||||
onItemsRendered={onItemsRendered}
|
||||
>
|
||||
{Item}
|
||||
</VariableSizeList>
|
||||
)}
|
||||
</InfiniteLoader>
|
||||
)}
|
||||
</AutoSizer>
|
||||
</div>
|
||||
<div
|
||||
style={{
|
||||
position: 'absolute',
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { useQuery } from '@tanstack/react-query'
|
||||
import { get, handleError } from 'data/fetchers'
|
||||
import { IS_PLATFORM } from 'lib/constants'
|
||||
import { analyticsKeys } from './keys'
|
||||
|
||||
type WarehouseCollectionsVariables = {
|
||||
@@ -35,6 +36,6 @@ export const useWarehouseCollectionsQuery = (
|
||||
analyticsKeys.warehouseCollections(projectRef),
|
||||
({ signal }) => getWarehouseCollections({ projectRef }, signal),
|
||||
{
|
||||
enabled: enabled && !!projectRef,
|
||||
enabled: enabled && !!projectRef && IS_PLATFORM,
|
||||
}
|
||||
)
|
||||
@@ -1,7 +1,8 @@
|
||||
import { useQuery, UseQueryOptions } from '@tanstack/react-query'
|
||||
import { analyticsKeys } from './keys'
|
||||
import { get, handleError } from 'data/fetchers'
|
||||
import { IS_PLATFORM } from 'lib/constants'
|
||||
import { ResponseError } from 'types'
|
||||
import { analyticsKeys } from './keys'
|
||||
|
||||
export type WarehouseTenantVariables = {
|
||||
projectRef: string
|
||||
@@ -41,7 +42,7 @@ export const useWarehouseTenantQuery = <TData = WarehouseTenantData>(
|
||||
analyticsKeys.warehouseTenant(projectRef),
|
||||
({ signal }) => getWarehouseTenant({ projectRef }, signal),
|
||||
{
|
||||
enabled: enabled && !!projectRef,
|
||||
enabled: enabled && !!projectRef && IS_PLATFORM,
|
||||
staleTime: Infinity,
|
||||
// 2H mins cache time
|
||||
cacheTime: 120 * 60 * 1000,
|
||||
|
||||
@@ -1,25 +1,22 @@
|
||||
import { useQuery, UseQueryOptions } from '@tanstack/react-query'
|
||||
|
||||
import { get } from 'lib/common/fetch'
|
||||
import { API_URL } from 'lib/constants'
|
||||
import { get, handleError } from 'data/fetchers'
|
||||
import type { ResponseError } from 'types'
|
||||
import type { Integration, IntegrationsVariables } from './integrations.types'
|
||||
import { Integration } from './integrations.types'
|
||||
import { integrationKeys } from './keys'
|
||||
|
||||
export type IntegrationsResponse = Integration[]
|
||||
type IntegrationsVariables = {
|
||||
orgSlug?: string
|
||||
}
|
||||
|
||||
export async function getIntegrations({ orgSlug }: IntegrationsVariables, signal?: AbortSignal) {
|
||||
if (!orgSlug) {
|
||||
throw new Error('orgSlug is required')
|
||||
}
|
||||
const response = await get(`${API_URL}/integrations/${orgSlug}?expand=true`, {
|
||||
signal,
|
||||
})
|
||||
if (response.error) {
|
||||
throw response.error
|
||||
}
|
||||
if (!orgSlug) throw new Error('orgSlug is required')
|
||||
|
||||
return response as IntegrationsResponse
|
||||
const { data, error } = await get('/platform/integrations/{slug}', {
|
||||
params: { path: { slug: orgSlug } },
|
||||
})
|
||||
if (error) handleError(error)
|
||||
return data as unknown as Integration[]
|
||||
}
|
||||
|
||||
export type IntegrationsData = Awaited<ReturnType<typeof getIntegrations>>
|
||||
|
||||
+10
-16
@@ -1,8 +1,8 @@
|
||||
import { useMutation, UseMutationOptions, useQueryClient } from '@tanstack/react-query'
|
||||
import { delete_, isResponseOk } from 'lib/common/fetch'
|
||||
import { API_URL } from 'lib/constants'
|
||||
import { toast } from 'sonner'
|
||||
import type { ResponseError, UserContent } from 'types'
|
||||
|
||||
import { del, handleError } from 'data/fetchers'
|
||||
import type { ResponseError } from 'types'
|
||||
import { integrationKeys } from './keys'
|
||||
|
||||
type DeleteVariables = {
|
||||
@@ -15,21 +15,15 @@ export async function deleteConnection(
|
||||
{ organization_integration_id, id }: DeleteVariables,
|
||||
signal?: AbortSignal
|
||||
) {
|
||||
if (!organization_integration_id) {
|
||||
throw new Error('organization_integration_id is required')
|
||||
}
|
||||
if (!organization_integration_id) throw new Error('organization_integration_id is required')
|
||||
|
||||
const response = await delete_<UserContent>(
|
||||
`${API_URL}/integrations/vercel/connections/${id}`,
|
||||
{ organization_integration_id },
|
||||
{ signal }
|
||||
)
|
||||
const { data, error } = await del('/platform/integrations/vercel/connections/{connection_id}', {
|
||||
params: { path: { connection_id: id } },
|
||||
signal,
|
||||
})
|
||||
|
||||
if (!isResponseOk(response)) {
|
||||
throw response.error
|
||||
}
|
||||
|
||||
return response
|
||||
if (error) handleError(error)
|
||||
return data
|
||||
}
|
||||
|
||||
type DeleteContentData = Awaited<ReturnType<typeof deleteConnection>>
|
||||
|
||||
@@ -160,10 +160,6 @@ export type IntegrationProjectConnection = {
|
||||
metadata: Imetadata
|
||||
}
|
||||
|
||||
export type IntegrationsVariables = {
|
||||
orgSlug?: string
|
||||
}
|
||||
|
||||
export type IntegrationProjectConnectionPayload = {
|
||||
foreignProjectId: string
|
||||
supabaseProjectId: string
|
||||
@@ -177,7 +173,6 @@ export type userDetails = {
|
||||
primary_email: string
|
||||
}
|
||||
type addedBy = userDetails
|
||||
type updatedBy = userDetails
|
||||
|
||||
export type IntegrationName = 'Vercel' | 'GitHub' // | 'Netlify'
|
||||
export type VercelAccountType = 'Team' | 'Personal'
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { useMutation, UseMutationOptions, useQueryClient } from '@tanstack/react-query'
|
||||
import { post } from 'lib/common/fetch'
|
||||
import { API_URL } from 'lib/constants'
|
||||
import { toast } from 'sonner'
|
||||
|
||||
import { handleError, post } from 'data/fetchers'
|
||||
import type { ResponseError } from 'types'
|
||||
import { integrationKeys } from './keys'
|
||||
|
||||
@@ -24,19 +24,21 @@ export async function createVercelIntegration({
|
||||
source,
|
||||
teamId,
|
||||
}: VercelIntegrationCreateVariables) {
|
||||
const response = await post(`${API_URL}/integrations/vercel`, {
|
||||
code,
|
||||
configuration_id: configurationId,
|
||||
organization_slug: orgSlug,
|
||||
metadata,
|
||||
source,
|
||||
teamId,
|
||||
const { data, error } = await post('/platform/integrations/vercel', {
|
||||
body: {
|
||||
code,
|
||||
configuration_id: configurationId,
|
||||
organization_slug: orgSlug,
|
||||
metadata: metadata as Record<string, never>,
|
||||
source,
|
||||
teamId,
|
||||
},
|
||||
})
|
||||
if (response.error) {
|
||||
throw response.error
|
||||
}
|
||||
|
||||
return response
|
||||
if (error) handleError(error)
|
||||
// [Joshen] API isn't typed on this endpoint
|
||||
// https://github.com/supabase/infrastructure/blob/develop/api/src/routes/platform/integrations/vercel/vercel-integration.controller.ts#L50
|
||||
return data as { id: string }
|
||||
}
|
||||
|
||||
type VercelIntegrationCreateData = Awaited<ReturnType<typeof createVercelIntegration>>
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
import { useMutation, UseMutationOptions, useQueryClient } from '@tanstack/react-query'
|
||||
import { toast } from 'sonner'
|
||||
|
||||
import { post } from 'lib/common/fetch'
|
||||
import { API_URL } from 'lib/constants'
|
||||
import { handleError, post } from 'data/fetchers'
|
||||
import type { ResponseError } from 'types'
|
||||
import { oauthAppKeys } from './keys'
|
||||
|
||||
@@ -15,9 +14,12 @@ export async function revokeAuthorizedApp({ id, slug }: AuthorizedAppRevokeVaria
|
||||
if (!id) throw new Error('App ID is required')
|
||||
if (!slug) throw new Error('Organization slug is required')
|
||||
|
||||
const response = await post(`${API_URL}/organizations/${slug}/oauth/apps/${id}/revoke`, {})
|
||||
if (response.error) throw response.error
|
||||
return response
|
||||
const { data, error } = await post('/platform/organizations/{slug}/oauth/apps/{id}/revoke', {
|
||||
params: { path: { slug, id } },
|
||||
})
|
||||
|
||||
if (error) handleError(error)
|
||||
return data
|
||||
}
|
||||
|
||||
type AuthorizedAppRevokeData = Awaited<ReturnType<typeof revokeAuthorizedApp>>
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { useQuery, UseQueryOptions } from '@tanstack/react-query'
|
||||
import { get } from 'lib/common/fetch'
|
||||
import { API_URL } from 'lib/constants'
|
||||
import { oauthAppKeys } from './keys'
|
||||
|
||||
import { get, handleError } from 'data/fetchers'
|
||||
import type { ResponseError } from 'types'
|
||||
import { oauthAppKeys } from './keys'
|
||||
|
||||
export type AuthorizedAppsVariables = {
|
||||
slug?: string
|
||||
@@ -19,11 +19,12 @@ export type AuthorizedApp = {
|
||||
export async function getAuthorizedApps({ slug }: AuthorizedAppsVariables, signal?: AbortSignal) {
|
||||
if (!slug) throw new Error('Organization slug is required')
|
||||
|
||||
const response = await get(`${API_URL}/organizations/${slug}/oauth/apps?type=authorized`, {
|
||||
signal,
|
||||
const { data, error } = await get('/platform/organizations/{slug}/oauth/apps', {
|
||||
params: { path: { slug }, query: { type: 'authorized' } },
|
||||
})
|
||||
if (response.error) throw response.error
|
||||
return response as AuthorizedApp[]
|
||||
|
||||
if (error) handleError(error)
|
||||
return data as AuthorizedApp[]
|
||||
}
|
||||
|
||||
export type AuthorizedAppsData = Awaited<ReturnType<typeof getAuthorizedApps>>
|
||||
|
||||
@@ -2,8 +2,8 @@ import type { OAuthScope } from '@supabase/shared-types/out/constants'
|
||||
import { useMutation, UseMutationOptions, useQueryClient } from '@tanstack/react-query'
|
||||
import { toast } from 'sonner'
|
||||
|
||||
import { post } from 'lib/common/fetch'
|
||||
import { API_URL } from 'lib/constants'
|
||||
import { components } from 'api-types'
|
||||
import { handleError, post } from 'data/fetchers'
|
||||
import type { ResponseError } from 'types'
|
||||
import { oauthAppKeys } from './keys'
|
||||
|
||||
@@ -16,11 +16,7 @@ export type OAuthAppCreateVariables = {
|
||||
redirect_uris: string[]
|
||||
}
|
||||
|
||||
export type OAuthAppCreateResponse = {
|
||||
id: string
|
||||
client_id: string
|
||||
client_secret: string
|
||||
}
|
||||
export type OAuthAppCreateResponse = components['schemas']['CreateOAuthAppResponse']
|
||||
|
||||
export async function createOAuthApp({
|
||||
slug,
|
||||
@@ -30,15 +26,19 @@ export async function createOAuthApp({
|
||||
scopes,
|
||||
redirect_uris,
|
||||
}: OAuthAppCreateVariables) {
|
||||
const response = await post(`${API_URL}/organizations/${slug}/oauth/apps`, {
|
||||
name,
|
||||
website,
|
||||
icon,
|
||||
scopes,
|
||||
redirect_uris,
|
||||
const { data, error } = await post('/platform/organizations/{slug}/oauth/apps', {
|
||||
params: { path: { slug } },
|
||||
body: {
|
||||
name,
|
||||
website,
|
||||
icon,
|
||||
scopes,
|
||||
redirect_uris,
|
||||
},
|
||||
})
|
||||
if (response.error) throw response.error
|
||||
return response as OAuthAppCreateResponse
|
||||
|
||||
if (error) handleError(error)
|
||||
return data
|
||||
}
|
||||
|
||||
type OAuthAppCreateData = Awaited<ReturnType<typeof createOAuthApp>>
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
import { useMutation, UseMutationOptions, useQueryClient } from '@tanstack/react-query'
|
||||
import { toast } from 'sonner'
|
||||
|
||||
import { delete_ } from 'lib/common/fetch'
|
||||
import { API_URL } from 'lib/constants'
|
||||
import { del, handleError } from 'data/fetchers'
|
||||
import type { ResponseError } from 'types'
|
||||
import { oauthAppKeys } from './keys'
|
||||
|
||||
@@ -15,9 +14,12 @@ export async function deleteOAuthApp({ id, slug }: OAuthAppDeleteVariables) {
|
||||
if (!id) throw new Error('OAuth app ID is required')
|
||||
if (!slug) throw new Error('Organization slug is required')
|
||||
|
||||
const response = await delete_(`${API_URL}/organizations/${slug}/oauth/apps/${id}?type=published`)
|
||||
if (response.error) throw response.error
|
||||
return response
|
||||
const { data, error } = await del('/platform/organizations/{slug}/oauth/apps/{id}', {
|
||||
params: { path: { slug, id } },
|
||||
})
|
||||
|
||||
if (error) handleError(error)
|
||||
return data
|
||||
}
|
||||
|
||||
type OAuthAppDeleteData = Awaited<ReturnType<typeof deleteOAuthApp>>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import type { OAuthScope } from '@supabase/shared-types/out/constants'
|
||||
import { useQuery, UseQueryOptions } from '@tanstack/react-query'
|
||||
import { get } from 'lib/common/fetch'
|
||||
import { API_URL } from 'lib/constants'
|
||||
|
||||
import { components } from 'api-types'
|
||||
import { get, handleError } from 'data/fetchers'
|
||||
import type { ResponseError } from 'types'
|
||||
import { oauthAppKeys } from './keys'
|
||||
|
||||
@@ -9,26 +9,17 @@ export type OAuthAppsVariables = {
|
||||
slug?: string
|
||||
}
|
||||
|
||||
export type OAuthApp = {
|
||||
id: string
|
||||
icon: string | null
|
||||
client_id: string
|
||||
client_secret_alias: string
|
||||
created_at: string
|
||||
name: string
|
||||
website: string
|
||||
scopes: OAuthScope[]
|
||||
redirect_uris: string[]
|
||||
}
|
||||
export type OAuthApp = components['schemas']['OAuthAppResponse']
|
||||
|
||||
export async function getOAuthApps({ slug }: OAuthAppsVariables, signal?: AbortSignal) {
|
||||
if (!slug) throw new Error('Organization slug is required')
|
||||
|
||||
const response = await get(`${API_URL}/organizations/${slug}/oauth/apps?type=published`, {
|
||||
signal,
|
||||
const { data, error } = await get('/platform/organizations/{slug}/oauth/apps', {
|
||||
params: { path: { slug }, query: { type: 'published' } },
|
||||
})
|
||||
if (response.error) throw response.error
|
||||
return response as OAuthApp[]
|
||||
|
||||
if (error) handleError(error)
|
||||
return data
|
||||
}
|
||||
|
||||
export type OAuthAppsData = Awaited<ReturnType<typeof getOAuthApps>>
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { useMutation, UseMutationOptions, useQueryClient } from '@tanstack/react-query'
|
||||
import { toast } from 'sonner'
|
||||
|
||||
import { post } from 'lib/common/fetch'
|
||||
import { API_URL } from 'lib/constants'
|
||||
import { handleError, post } from 'data/fetchers'
|
||||
import { ResponseError } from 'types'
|
||||
import { projectKeys } from './keys'
|
||||
|
||||
export type ProjectTransferVariables = {
|
||||
@@ -10,10 +10,6 @@ export type ProjectTransferVariables = {
|
||||
targetOrganizationSlug?: string
|
||||
}
|
||||
|
||||
type ProjectTransferError = {
|
||||
message: string
|
||||
}
|
||||
|
||||
export async function transferProject({
|
||||
projectRef,
|
||||
targetOrganizationSlug,
|
||||
@@ -25,10 +21,12 @@ export async function transferProject({
|
||||
target_organization_slug: targetOrganizationSlug,
|
||||
}
|
||||
|
||||
const response = await post(`${API_URL}/projects/${projectRef}/transfer`, payload)
|
||||
if (response.error) throw response.error
|
||||
|
||||
return response
|
||||
const { data, error } = await post('/platform/projects/{ref}/transfer', {
|
||||
params: { path: { ref: projectRef } },
|
||||
body: payload,
|
||||
})
|
||||
if (error) handleError(error)
|
||||
return data
|
||||
}
|
||||
|
||||
type ProjectTransferData = Awaited<ReturnType<typeof transferProject>>
|
||||
@@ -38,12 +36,12 @@ export const useProjectTransferMutation = ({
|
||||
onError,
|
||||
...options
|
||||
}: Omit<
|
||||
UseMutationOptions<ProjectTransferData, ProjectTransferError, ProjectTransferVariables>,
|
||||
UseMutationOptions<ProjectTransferData, ResponseError, ProjectTransferVariables>,
|
||||
'mutationFn'
|
||||
> = {}) => {
|
||||
const queryClient = useQueryClient()
|
||||
|
||||
return useMutation<ProjectTransferData, ProjectTransferError, ProjectTransferVariables>(
|
||||
return useMutation<ProjectTransferData, ResponseError, ProjectTransferVariables>(
|
||||
(vars) => transferProject(vars),
|
||||
{
|
||||
async onSuccess(data, variables, context) {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useQuery, UseQueryOptions } from '@tanstack/react-query'
|
||||
import { post } from 'lib/common/fetch'
|
||||
import { API_URL } from 'lib/constants'
|
||||
|
||||
import { handleError, post } from 'data/fetchers'
|
||||
import { projectKeys } from './keys'
|
||||
|
||||
export type ProjectTransferPreviewVariables = {
|
||||
@@ -10,40 +10,6 @@ export type ProjectTransferPreviewVariables = {
|
||||
|
||||
export type PlanId = 'free' | 'pro' | 'team' | 'enterprise'
|
||||
|
||||
type MemberExceedingFreeProjectLimit = {
|
||||
name: string
|
||||
limit: number
|
||||
}
|
||||
|
||||
type PreviewTransferInfo = {
|
||||
key: string
|
||||
message: string
|
||||
}
|
||||
|
||||
export type PreviewProjectTransferResponse = {
|
||||
valid: boolean
|
||||
|
||||
warnings: PreviewTransferInfo[]
|
||||
errors: PreviewTransferInfo[]
|
||||
|
||||
members_exceeding_free_project_limit: MemberExceedingFreeProjectLimit[]
|
||||
|
||||
has_permissions_on_source_organization: boolean
|
||||
has_access_to_target_organization: boolean
|
||||
|
||||
source_project_eligible: boolean
|
||||
|
||||
target_organization_eligible: boolean | null
|
||||
target_organization_has_free_project_slots: boolean | null
|
||||
|
||||
credits_on_source_organization: number
|
||||
costs_on_target_organization: number
|
||||
charge_on_target_organization: number
|
||||
|
||||
source_subscription_plan: PlanId
|
||||
target_subscription_plan: PlanId | null
|
||||
}
|
||||
|
||||
export async function previewProjectTransfer(
|
||||
{ projectRef, targetOrganizationSlug }: ProjectTransferPreviewVariables,
|
||||
signal?: AbortSignal
|
||||
@@ -51,16 +17,14 @@ export async function previewProjectTransfer(
|
||||
if (!projectRef) throw new Error('projectRef is required')
|
||||
if (!targetOrganizationSlug) throw new Error('targetOrganizationSlug is required')
|
||||
|
||||
const response = await post(
|
||||
`${API_URL}/projects/${projectRef}/transfer/preview`,
|
||||
{
|
||||
target_organization_slug: targetOrganizationSlug,
|
||||
},
|
||||
{ signal }
|
||||
)
|
||||
if (response.error) throw response.error
|
||||
const { data, error } = await post('/platform/projects/{ref}/transfer/preview', {
|
||||
params: { path: { ref: projectRef } },
|
||||
body: { target_organization_slug: targetOrganizationSlug },
|
||||
signal,
|
||||
})
|
||||
|
||||
return response as PreviewProjectTransferResponse
|
||||
if (error) handleError(error)
|
||||
return data
|
||||
}
|
||||
|
||||
export type ProjectTransferPreviewData = Awaited<ReturnType<typeof previewProjectTransfer>>
|
||||
|
||||
@@ -2,6 +2,7 @@ import { useQuery, UseQueryOptions } from '@tanstack/react-query'
|
||||
|
||||
import type { components } from 'data/api'
|
||||
import { get, handleError } from 'data/fetchers'
|
||||
import { IS_PLATFORM } from 'lib/constants'
|
||||
import type { ResponseError } from 'types'
|
||||
import { replicaKeys } from './keys'
|
||||
|
||||
@@ -37,7 +38,7 @@ export const useLoadBalancersQuery = <TData = LoadBalancersData>(
|
||||
replicaKeys.loadBalancers(projectRef),
|
||||
({ signal }) => getLoadBalancers({ projectRef }, signal),
|
||||
{
|
||||
enabled: enabled && typeof projectRef !== 'undefined',
|
||||
enabled: enabled && typeof projectRef !== 'undefined' && IS_PLATFORM,
|
||||
...options,
|
||||
}
|
||||
)
|
||||
|
||||
@@ -2,6 +2,7 @@ import { useQuery, UseQueryOptions } from '@tanstack/react-query'
|
||||
|
||||
import type { components } from 'data/api'
|
||||
import { get, handleError } from 'data/fetchers'
|
||||
import { IS_PLATFORM } from 'lib/constants'
|
||||
import type { ResponseError } from 'types'
|
||||
import { usageKeys } from './keys'
|
||||
|
||||
@@ -42,7 +43,7 @@ export const useOrgUsageQuery = <TData = OrgUsageData>(
|
||||
usageKeys.orgUsage(orgSlug, projectRef, start?.toISOString(), end?.toISOString()),
|
||||
({ signal }) => getOrgUsage({ orgSlug, projectRef, start, end }, signal),
|
||||
{
|
||||
enabled: enabled && typeof orgSlug !== 'undefined',
|
||||
enabled: enabled && IS_PLATFORM && typeof orgSlug !== 'undefined',
|
||||
staleTime: 1000 * 60 * 30, // 30 mins, underlying usage data only refreshes once an hour, so safe to cache for a while
|
||||
...options,
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@ export default () => ({
|
||||
metaDescription:
|
||||
'Supabase Cron is a Postgres Module that uses the pg_cron database extension to manage recurring tasks. Manage your Cron Jobs using any Postgres tooling.',
|
||||
metaImage: '/images/modules/cron/og.png',
|
||||
url: 'https://supabase.com/dashboard/project/_/integrations/cron-jobs/overview',
|
||||
url: 'https://supabase.com/dashboard/project/_/integrations/cron/overview',
|
||||
docsUrl: '/docs/guides/cron',
|
||||
heroSection: {
|
||||
title: 'Supabase Cron',
|
||||
@@ -22,7 +22,7 @@ export default () => ({
|
||||
icon: PRODUCT_MODULES['cron'].icon[24],
|
||||
cta: {
|
||||
label: 'Schedule your first Job',
|
||||
link: 'https://supabase.com/dashboard/project/_/integrations/cron-jobs/overview',
|
||||
link: 'https://supabase.com/dashboard/project/_/integrations/cron/overview',
|
||||
},
|
||||
secondaryCta: {
|
||||
label: 'Explore documentation',
|
||||
|
||||
@@ -17,8 +17,10 @@
|
||||
"zod": "^3.22.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/pg": "^8.11.11",
|
||||
"npm-run-all": "^4.1.5",
|
||||
"postgres": "^3.4.5",
|
||||
"pg": "^8.13.1",
|
||||
"postgres-array": "^3.0.2",
|
||||
"typescript": "~5.5.0",
|
||||
"vitest": "^2.1.8"
|
||||
}
|
||||
|
||||
@@ -36,11 +36,11 @@ export const pgFunctionZod = z.object({
|
||||
config_params: z.union([z.record(z.string(), z.string()), z.null()]),
|
||||
})
|
||||
|
||||
export type PGFunction = z.infer<typeof pgFunctionZod>
|
||||
|
||||
export const pgFunctionArrayZod = z.array(pgFunctionZod)
|
||||
export const pgFunctionOptionalZod = z.optional(pgFunctionZod)
|
||||
|
||||
export type PGFunction = z.infer<typeof pgFunctionZod>
|
||||
|
||||
export function list({
|
||||
includeSystemSchemas = false,
|
||||
includedSchemas,
|
||||
@@ -86,10 +86,12 @@ export function list({
|
||||
}
|
||||
}
|
||||
|
||||
export function retrieve({ id }: { id: number }): {
|
||||
type FunctionsRetrieveReturn = {
|
||||
sql: string
|
||||
zod: typeof pgFunctionOptionalZod
|
||||
}
|
||||
|
||||
export function retrieve({ id }: { id: number }): FunctionsRetrieveReturn
|
||||
export function retrieve({
|
||||
name,
|
||||
schema,
|
||||
@@ -98,10 +100,7 @@ export function retrieve({
|
||||
name: string
|
||||
schema: string
|
||||
args: string[]
|
||||
}): {
|
||||
sql: string
|
||||
zod: typeof pgFunctionOptionalZod
|
||||
}
|
||||
}): FunctionsRetrieveReturn
|
||||
export function retrieve({
|
||||
id,
|
||||
name,
|
||||
@@ -112,10 +111,7 @@ export function retrieve({
|
||||
name?: string
|
||||
schema?: string
|
||||
args?: string[]
|
||||
}): {
|
||||
sql: string
|
||||
zod: typeof pgFunctionOptionalZod
|
||||
} {
|
||||
}): FunctionsRetrieveReturn {
|
||||
if (id) {
|
||||
const sql = /* SQL */ `
|
||||
with f as (
|
||||
@@ -212,8 +208,8 @@ function _generateCreateFunctionSql(
|
||||
config_params
|
||||
? Object.entries(config_params)
|
||||
.map(
|
||||
([param, value]: string[]) =>
|
||||
`SET ${param} ${value[0] === 'FROM CURRENT' ? 'FROM CURRENT' : 'TO ' + (value === '""' ? "''" : value)}`
|
||||
([param, value]) =>
|
||||
`SET ${param} ${value === 'FROM CURRENT' ? 'FROM CURRENT' : 'TO ' + (value === '""' ? "''" : value)}`
|
||||
)
|
||||
.join('\n')
|
||||
: ''
|
||||
|
||||
@@ -80,7 +80,8 @@ from
|
||||
'mode', t2.mode,
|
||||
'name', name,
|
||||
'type_id', type_id,
|
||||
'has_default', has_default
|
||||
-- Cast null into false boolean
|
||||
'has_default', COALESCE(has_default, false)
|
||||
)) as args
|
||||
from
|
||||
(
|
||||
@@ -88,7 +89,9 @@ from
|
||||
oid,
|
||||
unnest(arg_modes) as mode,
|
||||
unnest(arg_names) as name,
|
||||
unnest(arg_types) as type_id,
|
||||
-- Coming from: coalesce(p.proallargtypes, p.proargtypes) postgres won't automatically assume
|
||||
-- integer, we need to cast it to be properly parsed
|
||||
unnest(arg_types)::int8 as type_id,
|
||||
unnest(arg_has_defaults) as has_default
|
||||
from
|
||||
functions
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
|
||||
-- We apply our test seedings to template1 so every new created db will have the same structure
|
||||
\c template1
|
||||
|
||||
-- Tables for testing
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
|
||||
-- We apply our test seedings to template1 so every new created db will have the same structure
|
||||
\c template1
|
||||
|
||||
CREATE TABLE public.category (
|
||||
id serial NOT NULL PRIMARY KEY,
|
||||
@@ -2431,3 +2432,5 @@ INSERT INTO public.memes (name, category, created_at) VALUES
|
||||
('Drinking Out of Cups', 4, NOW()),
|
||||
('Sonic the Hedgehog (2020 Film)', 4, NOW()),
|
||||
('Art Student Owl', 4, NOW());
|
||||
|
||||
\c postgres
|
||||
@@ -1,32 +1,57 @@
|
||||
import postgres from 'postgres'
|
||||
import pg, { Pool } from 'pg'
|
||||
import { randomUUID } from 'crypto'
|
||||
import { parse as parseArray } from 'postgres-array'
|
||||
|
||||
// Those types override are in sync with `postgres-meta` since the queries
|
||||
// will get executed via `execQuery` on a pg connection with the same configuration
|
||||
// see: https://github.com/supabase/postgres-meta/blob/ca06061b4708971628134f95e49f254c2dfdfa7d/src/lib/db.ts#L6-L23
|
||||
pg.types.setTypeParser(pg.types.builtins.INT8, (x) => {
|
||||
const asNumber = Number(x)
|
||||
if (Number.isSafeInteger(asNumber)) {
|
||||
return asNumber
|
||||
} else {
|
||||
return x
|
||||
}
|
||||
})
|
||||
pg.types.setTypeParser(pg.types.builtins.DATE, (x) => x)
|
||||
pg.types.setTypeParser(pg.types.builtins.INTERVAL, (x) => x)
|
||||
pg.types.setTypeParser(pg.types.builtins.TIMESTAMP, (x) => x)
|
||||
pg.types.setTypeParser(pg.types.builtins.TIMESTAMPTZ, (x) => x)
|
||||
pg.types.setTypeParser(1115, parseArray) // _timestamp
|
||||
pg.types.setTypeParser(1182, parseArray) // _date
|
||||
pg.types.setTypeParser(1185, parseArray) // _timestamptz
|
||||
pg.types.setTypeParser(600, (x) => x) // point
|
||||
pg.types.setTypeParser(1017, (x) => x) // _point
|
||||
|
||||
const ROOT_DB_URL = process.env.DATABASE_URL ?? 'postgresql://postgres:postgres@localhost:5432'
|
||||
const ROOT_DB_NAME = process.env.DATABASE_NAME ?? 'postgres'
|
||||
// Connection to the postgres server (not a specific database)
|
||||
const rootDb = postgres(`${ROOT_DB_URL}/${ROOT_DB_NAME}`)
|
||||
|
||||
// Replace postgres.js root connection with pg connection
|
||||
const rootPool = new Pool({
|
||||
connectionString: `${ROOT_DB_URL}/${ROOT_DB_NAME}`,
|
||||
max: 1,
|
||||
})
|
||||
|
||||
export async function createTestDatabase() {
|
||||
const dbName = `test_${randomUUID().replace(/-/g, '_')}`
|
||||
|
||||
try {
|
||||
// Create a new database for this test
|
||||
await rootDb.unsafe(`CREATE DATABASE ${dbName};`)
|
||||
await rootPool.query(`CREATE DATABASE ${dbName};`)
|
||||
|
||||
// Create a new connection to the test database
|
||||
const testDb = postgres(`${ROOT_DB_URL}/${dbName}`, {
|
||||
const pool = new Pool({
|
||||
connectionString: `${ROOT_DB_URL}/${dbName}`,
|
||||
max: 1,
|
||||
idle_timeout: 20,
|
||||
connect_timeout: 10,
|
||||
idleTimeoutMillis: 20000,
|
||||
connectionTimeoutMillis: 10000,
|
||||
})
|
||||
|
||||
// Return both the database name and the client
|
||||
return {
|
||||
dbName,
|
||||
client: 'pg' as const,
|
||||
executeQuery: async <T = any>(query: string): Promise<T> => {
|
||||
try {
|
||||
const result = (await testDb.unsafe(query)).map((v) => v)
|
||||
return result as T
|
||||
const res = await pool.query(query)
|
||||
return res.rows as T
|
||||
} catch (error) {
|
||||
if (error instanceof Error) {
|
||||
throw new Error(`Failed to execute query: ${error.message}`)
|
||||
@@ -35,10 +60,8 @@ export async function createTestDatabase() {
|
||||
}
|
||||
},
|
||||
cleanup: async () => {
|
||||
// Close the test database connection
|
||||
await testDb.end()
|
||||
// Drop the test database
|
||||
await rootDb.unsafe(`DROP DATABASE ${dbName};`)
|
||||
await pool.end()
|
||||
await rootPool.query(`DROP DATABASE ${dbName};`)
|
||||
},
|
||||
}
|
||||
} catch (error) {
|
||||
@@ -49,7 +72,7 @@ export async function createTestDatabase() {
|
||||
}
|
||||
}
|
||||
|
||||
// Cleanup the root connection when all tests are done
|
||||
// Update cleanup function to use pg pool
|
||||
export async function cleanupRoot() {
|
||||
await rootDb.end()
|
||||
await rootPool.end()
|
||||
}
|
||||
@@ -0,0 +1,343 @@
|
||||
import { expect, test, beforeAll, afterAll } from 'vitest'
|
||||
import pgMeta from '../src/index'
|
||||
import { createTestDatabase, cleanupRoot } from './db/utils'
|
||||
|
||||
beforeAll(async () => {
|
||||
// Any global setup if needed
|
||||
})
|
||||
|
||||
afterAll(async () => {
|
||||
await cleanupRoot()
|
||||
})
|
||||
|
||||
const withTestDatabase = (
|
||||
name: string,
|
||||
fn: (db: Awaited<ReturnType<typeof createTestDatabase>>) => Promise<void>
|
||||
) => {
|
||||
test(name, async () => {
|
||||
const db = await createTestDatabase()
|
||||
try {
|
||||
await fn(db)
|
||||
} finally {
|
||||
await db.cleanup()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
withTestDatabase('list functions', async ({ executeQuery }) => {
|
||||
const { sql, zod } = await pgMeta.functions.list()
|
||||
const res = zod.parse(await executeQuery(sql))
|
||||
|
||||
// Test for the 'add' function created in init.sql
|
||||
const addFunction = res.find(({ name }) => name === 'add')
|
||||
expect(addFunction).toMatchInlineSnapshot(
|
||||
{ id: expect.any(Number) },
|
||||
`
|
||||
{
|
||||
"args": [
|
||||
{
|
||||
"has_default": false,
|
||||
"mode": "in",
|
||||
"name": "",
|
||||
"type_id": 23,
|
||||
},
|
||||
{
|
||||
"has_default": false,
|
||||
"mode": "in",
|
||||
"name": "",
|
||||
"type_id": 23,
|
||||
},
|
||||
],
|
||||
"argument_types": "integer, integer",
|
||||
"behavior": "IMMUTABLE",
|
||||
"complete_statement": "CREATE OR REPLACE FUNCTION public.add(integer, integer)
|
||||
RETURNS integer
|
||||
LANGUAGE sql
|
||||
IMMUTABLE STRICT
|
||||
AS $function$select $1 + $2;$function$
|
||||
",
|
||||
"config_params": null,
|
||||
"definition": "select $1 + $2;",
|
||||
"id": Any<Number>,
|
||||
"identity_argument_types": "integer, integer",
|
||||
"is_set_returning_function": false,
|
||||
"language": "sql",
|
||||
"name": "add",
|
||||
"return_type": "integer",
|
||||
"return_type_id": 23,
|
||||
"return_type_relation_id": null,
|
||||
"schema": "public",
|
||||
"security_definer": false,
|
||||
}
|
||||
`
|
||||
)
|
||||
})
|
||||
|
||||
withTestDatabase('list functions with included schemas', async ({ executeQuery }) => {
|
||||
const { sql, zod } = await pgMeta.functions.list({
|
||||
includedSchemas: ['public'],
|
||||
})
|
||||
const res = zod.parse(await executeQuery(sql))
|
||||
|
||||
expect(res.length).toBeGreaterThan(0)
|
||||
res.forEach((func) => {
|
||||
expect(func.schema).toBe('public')
|
||||
})
|
||||
})
|
||||
|
||||
withTestDatabase('list functions with excluded schemas', async ({ executeQuery }) => {
|
||||
const { sql, zod } = await pgMeta.functions.list({
|
||||
excludedSchemas: ['public'],
|
||||
})
|
||||
const res = zod.parse(await executeQuery(sql))
|
||||
|
||||
res.forEach((func) => {
|
||||
expect(func.schema).not.toBe('public')
|
||||
})
|
||||
})
|
||||
|
||||
withTestDatabase(
|
||||
'list functions with excluded schemas and include System Schemas',
|
||||
async ({ executeQuery }) => {
|
||||
const { sql, zod } = await pgMeta.functions.list({
|
||||
excludedSchemas: ['public'],
|
||||
includeSystemSchemas: true,
|
||||
})
|
||||
const res = zod.parse(await executeQuery(sql))
|
||||
|
||||
expect(res.length).toBeGreaterThan(0)
|
||||
res.forEach((func) => {
|
||||
expect(func.schema).not.toBe('public')
|
||||
})
|
||||
}
|
||||
)
|
||||
|
||||
withTestDatabase('retrieve, create, update, delete', async ({ executeQuery }) => {
|
||||
// Create function
|
||||
const { sql: createSql } = await pgMeta.functions.create({
|
||||
name: 'test_func',
|
||||
schema: 'public',
|
||||
args: ['a int2', 'b int2'],
|
||||
definition: 'select a + b',
|
||||
return_type: 'integer',
|
||||
language: 'sql',
|
||||
behavior: 'STABLE',
|
||||
security_definer: true,
|
||||
config_params: { search_path: 'hooks, auth', role: 'postgres' },
|
||||
})
|
||||
await executeQuery(createSql)
|
||||
|
||||
// // Retrieve function
|
||||
const { sql: retrieveSql, zod: retrieveZod } = await pgMeta.functions.retrieve({
|
||||
name: 'test_func',
|
||||
schema: 'public',
|
||||
args: ['a int2', 'b int2'],
|
||||
})
|
||||
const retrieve = await executeQuery(retrieveSql)
|
||||
const res = retrieveZod.parse(retrieve[0])
|
||||
const functionId = res!.id
|
||||
expect({ data: res, error: null }).toMatchInlineSnapshot(
|
||||
{ data: { id: expect.any(Number) } },
|
||||
`
|
||||
{
|
||||
"data": {
|
||||
"args": [
|
||||
{
|
||||
"has_default": false,
|
||||
"mode": "in",
|
||||
"name": "a",
|
||||
"type_id": 21,
|
||||
},
|
||||
{
|
||||
"has_default": false,
|
||||
"mode": "in",
|
||||
"name": "b",
|
||||
"type_id": 21,
|
||||
},
|
||||
],
|
||||
"argument_types": "a smallint, b smallint",
|
||||
"behavior": "STABLE",
|
||||
"complete_statement": "CREATE OR REPLACE FUNCTION public.test_func(a smallint, b smallint)
|
||||
RETURNS integer
|
||||
LANGUAGE sql
|
||||
STABLE SECURITY DEFINER
|
||||
SET search_path TO 'hooks', 'auth'
|
||||
SET role TO 'postgres'
|
||||
AS $function$select a + b$function$
|
||||
",
|
||||
"config_params": {
|
||||
"role": "postgres",
|
||||
"search_path": "hooks, auth",
|
||||
},
|
||||
"definition": "select a + b",
|
||||
"id": Any<Number>,
|
||||
"identity_argument_types": "a smallint, b smallint",
|
||||
"is_set_returning_function": false,
|
||||
"language": "sql",
|
||||
"name": "test_func",
|
||||
"return_type": "integer",
|
||||
"return_type_id": 23,
|
||||
"return_type_relation_id": null,
|
||||
"schema": "public",
|
||||
"security_definer": true,
|
||||
},
|
||||
"error": null,
|
||||
}
|
||||
`
|
||||
)
|
||||
// create test_schema to move the function into:
|
||||
const { sql: createSchemaSql } = await pgMeta.schemas.create({ name: 'test_schema' })
|
||||
await executeQuery(createSchemaSql)
|
||||
const { sql: updateSql } = await pgMeta.functions.update(res!, {
|
||||
name: 'test_func_renamed',
|
||||
schema: 'test_schema',
|
||||
definition: 'select b - a',
|
||||
})
|
||||
await executeQuery(updateSql)
|
||||
|
||||
const { sql: retrieveRenamedSql } = await pgMeta.functions.retrieve({ id: functionId })
|
||||
const retrieveRenamed = await executeQuery(retrieveRenamedSql)
|
||||
const resUpdated = retrieveZod.parse(retrieveRenamed[0])
|
||||
expect({ data: resUpdated, error: null }).toMatchInlineSnapshot(
|
||||
{ data: { id: expect.any(Number) } },
|
||||
`
|
||||
{
|
||||
"data": {
|
||||
"args": [
|
||||
{
|
||||
"has_default": false,
|
||||
"mode": "in",
|
||||
"name": "a",
|
||||
"type_id": 21,
|
||||
},
|
||||
{
|
||||
"has_default": false,
|
||||
"mode": "in",
|
||||
"name": "b",
|
||||
"type_id": 21,
|
||||
},
|
||||
],
|
||||
"argument_types": "a smallint, b smallint",
|
||||
"behavior": "STABLE",
|
||||
"complete_statement": "CREATE OR REPLACE FUNCTION test_schema.test_func_renamed(a smallint, b smallint)
|
||||
RETURNS integer
|
||||
LANGUAGE sql
|
||||
STABLE SECURITY DEFINER
|
||||
SET role TO 'postgres'
|
||||
SET search_path TO 'hooks', 'auth'
|
||||
AS $function$select b - a$function$
|
||||
",
|
||||
"config_params": {
|
||||
"role": "postgres",
|
||||
"search_path": "hooks, auth",
|
||||
},
|
||||
"definition": "select b - a",
|
||||
"id": Any<Number>,
|
||||
"identity_argument_types": "a smallint, b smallint",
|
||||
"is_set_returning_function": false,
|
||||
"language": "sql",
|
||||
"name": "test_func_renamed",
|
||||
"return_type": "integer",
|
||||
"return_type_id": 23,
|
||||
"return_type_relation_id": null,
|
||||
"schema": "test_schema",
|
||||
"security_definer": true,
|
||||
},
|
||||
"error": null,
|
||||
}
|
||||
`
|
||||
)
|
||||
|
||||
// Remove function
|
||||
const { sql: removeSql } = await pgMeta.functions.remove(resUpdated!)
|
||||
await executeQuery(removeSql)
|
||||
// Verify function is removed
|
||||
const { sql: verifyRemoveSql } = await pgMeta.functions.retrieve({ id: functionId })
|
||||
const result = await executeQuery(verifyRemoveSql)
|
||||
expect(result).toHaveLength(0)
|
||||
})
|
||||
|
||||
withTestDatabase('retrieve set-returning function', async ({ executeQuery }) => {
|
||||
// Retrieve function
|
||||
const { sql: retrieveSql, zod: retrieveZod } = await pgMeta.functions.retrieve({
|
||||
schema: 'public',
|
||||
name: 'function_returning_set_of_rows',
|
||||
args: [],
|
||||
})
|
||||
const retrieve = await executeQuery(retrieveSql)
|
||||
const res = retrieveZod.parse(retrieve[0])
|
||||
expect(res).toMatchInlineSnapshot(
|
||||
{
|
||||
id: expect.any(Number),
|
||||
return_type_id: expect.any(Number),
|
||||
return_type_relation_id: expect.any(Number),
|
||||
},
|
||||
`
|
||||
{
|
||||
"args": [],
|
||||
"argument_types": "",
|
||||
"behavior": "STABLE",
|
||||
"complete_statement": "CREATE OR REPLACE FUNCTION public.function_returning_set_of_rows()
|
||||
RETURNS SETOF users
|
||||
LANGUAGE sql
|
||||
STABLE
|
||||
AS $function$
|
||||
select * from public.users;
|
||||
$function$
|
||||
",
|
||||
"config_params": null,
|
||||
"definition": "
|
||||
select * from public.users;
|
||||
",
|
||||
"id": Any<Number>,
|
||||
"identity_argument_types": "",
|
||||
"is_set_returning_function": true,
|
||||
"language": "sql",
|
||||
"name": "function_returning_set_of_rows",
|
||||
"return_type": "SETOF users",
|
||||
"return_type_id": Any<Number>,
|
||||
"return_type_relation_id": Any<Number>,
|
||||
"schema": "public",
|
||||
"security_definer": false,
|
||||
}
|
||||
`
|
||||
)
|
||||
})
|
||||
|
||||
withTestDatabase('create function with various config_params values', async ({ executeQuery }) => {
|
||||
// Set initial application_name for consistent testing
|
||||
await executeQuery("SET application_name = 'current-app-name'")
|
||||
|
||||
const { sql: createSql1 } = await pgMeta.functions.create({
|
||||
name: 'test_func_config_1',
|
||||
schema: 'public',
|
||||
definition: 'select 1',
|
||||
return_type: 'integer',
|
||||
language: 'sql',
|
||||
config_params: {
|
||||
search_path: '""', // Should become ''
|
||||
application_name: 'FROM CURRENT', // Special syntax: SET param FROM CURRENT
|
||||
work_mem: "'8MB'", // Regular syntax: SET param TO value
|
||||
},
|
||||
})
|
||||
await executeQuery(createSql1)
|
||||
|
||||
// Verify the function was created correctly
|
||||
const { sql: retrieveSql1, zod: retrieveZod } = await pgMeta.functions.retrieve({
|
||||
name: 'test_func_config_1',
|
||||
schema: 'public',
|
||||
args: [],
|
||||
})
|
||||
const result1 = retrieveZod.parse((await executeQuery(retrieveSql1))[0])
|
||||
|
||||
expect(result1).toBeDefined()
|
||||
expect(result1!.config_params).toEqual({
|
||||
search_path: '""',
|
||||
application_name: 'current-app-name',
|
||||
work_mem: '8MB',
|
||||
})
|
||||
|
||||
// Clean up
|
||||
const { sql: removeSql1 } = await pgMeta.functions.remove(result1!)
|
||||
await executeQuery(removeSql1)
|
||||
})
|
||||
@@ -1,7 +1,8 @@
|
||||
{
|
||||
"extends": "tsconfig/base.json",
|
||||
"extends": "../tsconfig/base.json",
|
||||
"compilerOptions": {
|
||||
"module": "ESNext"
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "bundler"
|
||||
},
|
||||
"include": ["."],
|
||||
"exclude": ["dist", "build", "node_modules"]
|
||||
|
||||
@@ -39,6 +39,8 @@ const AssistantCommandsPopover = forwardRef<
|
||||
suggestions?: string[]
|
||||
/* Whether an icon is being used in the AssistantChatForm */
|
||||
usingIcon?: boolean
|
||||
/* Available commands in suggestions popover */
|
||||
commands?: string[]
|
||||
}
|
||||
>(
|
||||
(
|
||||
@@ -51,6 +53,7 @@ const AssistantCommandsPopover = forwardRef<
|
||||
setOpen,
|
||||
suggestions,
|
||||
usingIcon = false,
|
||||
commands = ['fix', 'improve', 'explain', 'help'],
|
||||
...props
|
||||
},
|
||||
ref
|
||||
@@ -116,8 +119,6 @@ const AssistantCommandsPopover = forwardRef<
|
||||
|
||||
const resultArray = value.split(/(\s+)/).filter(Boolean)
|
||||
|
||||
const commands = ['fix', 'improve', 'explain', 'help']
|
||||
|
||||
return (
|
||||
<>
|
||||
<Popover
|
||||
|
||||
Generated
+154
-44
@@ -862,7 +862,7 @@ importers:
|
||||
version: 9.3.4
|
||||
'@testing-library/jest-dom':
|
||||
specifier: ^6.4.6
|
||||
version: 6.4.6(@jest/globals@29.7.0(supports-color@8.1.1))(@types/jest@29.5.5)(jest@29.7.0(@types/node@20.12.11)(supports-color@8.1.1))(vitest@3.0.4(@types/node@20.12.11)(@vitest/ui@3.0.4)(jsdom@20.0.3(supports-color@8.1.1))(msw@2.4.11(typescript@5.5.2))(sass@1.72.0)(supports-color@8.1.1)(terser@5.37.0))
|
||||
version: 6.4.6(@jest/globals@29.7.0(supports-color@8.1.1))(@types/jest@29.5.5)(jest@29.7.0(@types/node@20.12.11)(supports-color@8.1.1))(vitest@3.0.4)
|
||||
'@testing-library/react':
|
||||
specifier: ^14.0.0
|
||||
version: 14.0.0(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
|
||||
@@ -1390,7 +1390,7 @@ importers:
|
||||
dependencies:
|
||||
'@mertasan/tailwindcss-variables':
|
||||
specifier: ^2.2.3
|
||||
version: 2.7.0(autoprefixer@10.4.16(postcss@8.5.1))(postcss@8.5.1)
|
||||
version: 2.7.0(autoprefixer@10.4.16(postcss@8.4.38))(postcss@8.4.38)
|
||||
'@radix-ui/colors':
|
||||
specifier: ^0.1.8
|
||||
version: 0.1.9
|
||||
@@ -1475,12 +1475,18 @@ importers:
|
||||
specifier: ^3.22.4
|
||||
version: 3.23.8
|
||||
devDependencies:
|
||||
'@types/pg':
|
||||
specifier: ^8.11.11
|
||||
version: 8.11.11
|
||||
npm-run-all:
|
||||
specifier: ^4.1.5
|
||||
version: 4.1.5
|
||||
postgres:
|
||||
specifier: ^3.4.5
|
||||
version: 3.4.5
|
||||
pg:
|
||||
specifier: ^8.13.1
|
||||
version: 8.13.1
|
||||
postgres-array:
|
||||
specifier: ^3.0.2
|
||||
version: 3.0.2
|
||||
typescript:
|
||||
specifier: ~5.5.0
|
||||
version: 5.5.2
|
||||
@@ -1637,7 +1643,7 @@ importers:
|
||||
version: 0.436.0(react@18.2.0)
|
||||
next:
|
||||
specifier: 'catalog:'
|
||||
version: 14.2.21(@babel/core@7.24.7(supports-color@8.1.1))(@opentelemetry/api@1.9.0)(@playwright/test@1.49.1)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(sass@1.72.0)
|
||||
version: 14.2.21(@babel/core@7.26.7(supports-color@8.1.1))(@opentelemetry/api@1.9.0)(@playwright/test@1.49.1)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(sass@1.72.0)
|
||||
next-themes:
|
||||
specifier: ^0.3.0
|
||||
version: 0.3.0(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
|
||||
@@ -1737,7 +1743,7 @@ importers:
|
||||
version: 3.6.1
|
||||
'@testing-library/jest-dom':
|
||||
specifier: ^6.1.3
|
||||
version: 6.4.6(@jest/globals@29.7.0(supports-color@8.1.1))(@types/jest@29.5.5)(jest@29.7.0(@types/node@20.12.11)(supports-color@8.1.1))(vitest@3.0.4(@types/node@20.12.11)(@vitest/ui@3.0.4)(jsdom@20.0.3(supports-color@8.1.1))(msw@2.4.11(typescript@5.5.2))(sass@1.72.0)(supports-color@8.1.1)(terser@5.37.0))
|
||||
version: 6.4.6(@jest/globals@29.7.0(supports-color@8.1.1))(@types/jest@29.5.5)(jest@29.7.0(@types/node@20.12.11)(supports-color@8.1.1))(vitest@3.0.4)
|
||||
'@testing-library/react':
|
||||
specifier: ^14.0.0
|
||||
version: 14.0.0(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
|
||||
@@ -1836,7 +1842,7 @@ importers:
|
||||
version: 0.33.0
|
||||
next:
|
||||
specifier: 'catalog:'
|
||||
version: 14.2.21(@babel/core@7.24.7(supports-color@8.1.1))(@opentelemetry/api@1.9.0)(@playwright/test@1.49.1)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(sass@1.72.0)
|
||||
version: 14.2.21(@babel/core@7.26.7(supports-color@8.1.1))(@opentelemetry/api@1.9.0)(@playwright/test@1.49.1)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(sass@1.72.0)
|
||||
next-themes:
|
||||
specifier: '*'
|
||||
version: 0.3.0(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
|
||||
@@ -1906,7 +1912,7 @@ importers:
|
||||
version: 10.1.0
|
||||
'@testing-library/jest-dom':
|
||||
specifier: ^6.4.6
|
||||
version: 6.4.6(@jest/globals@29.7.0(supports-color@8.1.1))(@types/jest@29.5.5)(jest@29.7.0(@types/node@20.12.11)(supports-color@8.1.1))(vitest@3.0.4(@types/node@20.12.11)(@vitest/ui@3.0.4)(jsdom@20.0.3(supports-color@8.1.1))(msw@2.4.11(typescript@5.5.2))(sass@1.72.0)(supports-color@8.1.1)(terser@5.37.0))
|
||||
version: 6.4.6(@jest/globals@29.7.0(supports-color@8.1.1))(@types/jest@29.5.5)(jest@29.7.0(@types/node@20.12.11)(supports-color@8.1.1))(vitest@3.0.4)
|
||||
'@testing-library/react':
|
||||
specifier: ^16.0.0
|
||||
version: 16.0.0(@testing-library/dom@10.1.0)(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
|
||||
@@ -1933,7 +1939,7 @@ importers:
|
||||
version: link:../api-types
|
||||
next-router-mock:
|
||||
specifier: ^0.9.13
|
||||
version: 0.9.13(next@14.2.21(@babel/core@7.24.7(supports-color@8.1.1))(@opentelemetry/api@1.9.0)(@playwright/test@1.49.1)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(sass@1.72.0))(react@18.2.0)
|
||||
version: 0.9.13(next@14.2.21(@babel/core@7.26.7(supports-color@8.1.1))(@opentelemetry/api@1.9.0)(@playwright/test@1.49.1)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(sass@1.72.0))(react@18.2.0)
|
||||
typescript:
|
||||
specifier: ~5.5.0
|
||||
version: 5.5.2
|
||||
@@ -5769,6 +5775,9 @@ packages:
|
||||
'@types/pg-pool@2.0.6':
|
||||
resolution: {integrity: sha512-TaAUE5rq2VQYxab5Ts7WZhKNmuN78Q6PiFonTDdpbx8a1H0M1vhy3rhiMjl+e2iHmogyMw7jZF4FrE6eJUy5HQ==}
|
||||
|
||||
'@types/pg@8.11.11':
|
||||
resolution: {integrity: sha512-kGT1qKM8wJQ5qlawUrEkXgvMSXoV213KfMGXcwfDwUIfUHXqXYXOfS1nE1LINRJVVVx5wCm70XnFlMHaIcQAfw==}
|
||||
|
||||
'@types/pg@8.6.1':
|
||||
resolution: {integrity: sha512-1Kc4oAGzAl7uqUStZCDvaLFqZrW9qWSjXOmBfdgyBP5La7Us6Mg4GBvRlSoaZMhQF/zSj1C8CtKMBkoiT8eL8w==}
|
||||
|
||||
@@ -7625,7 +7634,6 @@ packages:
|
||||
resolution: {integrity: sha512-t0q23FIpvHDTtnORW+bDJziGsal5uh9RJTJ1fyH8drd4lICOoXhJ5pLMUZ5C0VQei6dNmwTzzoTRgMkO9JgHEQ==}
|
||||
peerDependencies:
|
||||
eslint: '>= 5'
|
||||
bundledDependencies: []
|
||||
|
||||
eslint-plugin-import@2.29.1:
|
||||
resolution: {integrity: sha512-BbPC0cuExzhiMo4Ff1BTVwHpjjv28C5R+btTOGaCRC7UEz801up0JadwkeSk5Ued6TG34uaczuVuH6qyy5YUxw==}
|
||||
@@ -10233,6 +10241,9 @@ packages:
|
||||
obliterator@2.0.4:
|
||||
resolution: {integrity: sha512-lgHwxlxV1qIg1Eap7LgIeoBWIMFibOjbrYPIPJZcI1mmGAI2m3lNYpK12Y+GBdPQ0U1hRwSord7GIaawz962qQ==}
|
||||
|
||||
obuf@1.1.2:
|
||||
resolution: {integrity: sha512-PX1wu0AmAdPqOL1mWhqmlOd8kOIZQwGZw6rh7uby9fTc5lhaOWFLX3I6R1hrF9k3zUY40e6igsLGkDXK92LJNg==}
|
||||
|
||||
on-exit-leak-free@2.1.2:
|
||||
resolution: {integrity: sha512-0eJJY6hXLGf1udHwfNftBqH+g73EU4B504nZeKpz1sYRKafAghwxEJunB2O7rDZkL4PGfsMVnTXZ2EjibbqcsA==}
|
||||
engines: {node: '>=14.0.0'}
|
||||
@@ -10438,6 +10449,9 @@ packages:
|
||||
pg-connection-string@2.6.2:
|
||||
resolution: {integrity: sha512-ch6OwaeaPYcova4kKZ15sbJ2hKb/VP48ZD2gE7i1J+L4MspCtBMAx8nMgz7bksc7IojCIIWuEhHibSMFH8m8oA==}
|
||||
|
||||
pg-connection-string@2.7.0:
|
||||
resolution: {integrity: sha512-PI2W9mv53rXJQEOb8xNR8lH7Hr+EKa6oJa38zsK0S/ky2er16ios1wLKhZyxzD7jUReiWokc9WK5nxSnC7W1TA==}
|
||||
|
||||
pg-format@1.0.4:
|
||||
resolution: {integrity: sha512-YyKEF78pEA6wwTAqOUaHIN/rWpfzzIuMh9KdAhc3rSLQ/7zkRFcCgYBAEGatDstLyZw4g0s9SNICmaTGnBVeyw==}
|
||||
engines: {node: '>=4.0'}
|
||||
@@ -10450,21 +10464,29 @@ packages:
|
||||
resolution: {integrity: sha512-NoSsPqXxbkD8RIe+peQCqiea4QzXgosdTKY8p7PsbbGsh2F8TifDj/vJxfuR8qJwNYrijdSs7uf0tAe6WOyCsQ==}
|
||||
engines: {node: '>=12.0.0'}
|
||||
|
||||
pg-pool@3.6.1:
|
||||
resolution: {integrity: sha512-jizsIzhkIitxCGfPRzJn1ZdcosIt3pz9Sh3V01fm1vZnbnCMgmGl5wvGGdNN2EL9Rmb0EcFoCkixH4Pu+sP9Og==}
|
||||
pg-numeric@1.0.2:
|
||||
resolution: {integrity: sha512-BM/Thnrw5jm2kKLE5uJkXqqExRUY/toLHda65XgFTBTFYZyopbKjBe29Ii3RbkvlsMoFwD+tHeGaCjjv0gHlyw==}
|
||||
engines: {node: '>=4'}
|
||||
|
||||
pg-pool@3.7.1:
|
||||
resolution: {integrity: sha512-xIOsFoh7Vdhojas6q3596mXFsR8nwBQBXX5JiV7p9buEVAGqYL4yFzclON5P9vFrpu1u7Zwl2oriyDa89n0wbw==}
|
||||
peerDependencies:
|
||||
pg: '>=8.0'
|
||||
|
||||
pg-protocol@1.6.0:
|
||||
resolution: {integrity: sha512-M+PDm637OY5WM307051+bsDia5Xej6d9IR4GwJse1qA1DIhiKlksvrneZOYQq42OM+spubpcNYEo2FcKQrDk+Q==}
|
||||
|
||||
pg-protocol@1.7.0:
|
||||
resolution: {integrity: sha512-hTK/mE36i8fDDhgDFjy6xNOG+LCorxLG3WO17tku+ij6sVHXh1jQUJ8hYAnRhNla4QVD2H8er/FOjc/+EgC6yQ==}
|
||||
pg-protocol@1.7.1:
|
||||
resolution: {integrity: sha512-gjTHWGYWsEgy9MsY0Gp6ZJxV24IjDqdpTW7Eh0x+WfJLFsm/TJx1MzL6T0D88mBvkpxotCQ6TwW6N+Kko7lhgQ==}
|
||||
|
||||
pg-types@2.2.0:
|
||||
resolution: {integrity: sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA==}
|
||||
engines: {node: '>=4'}
|
||||
|
||||
pg-types@4.0.2:
|
||||
resolution: {integrity: sha512-cRL3JpS3lKMGsKaWndugWQoLOCoP+Cic8oseVcbr0qhPzYD5DWXK+RZ9LY9wxRf7RQia4SCwQlXk0q6FCPrVng==}
|
||||
engines: {node: '>=10'}
|
||||
|
||||
pg@8.11.3:
|
||||
resolution: {integrity: sha512-+9iuvG8QfaaUrrph+kpF24cXkH1YOOUeArRNYIxq1viYHZagBxrTno7cecY1Fa44tJeZvaoG+Djpkc3JwehN5g==}
|
||||
engines: {node: '>= 8.0.0'}
|
||||
@@ -10474,6 +10496,15 @@ packages:
|
||||
pg-native:
|
||||
optional: true
|
||||
|
||||
pg@8.13.1:
|
||||
resolution: {integrity: sha512-OUir1A0rPNZlX//c7ksiu7crsGZTKSOXJPgtNiHGIlC9H0lO+NC6ZDYksSgBYY/thSWhnSRBv8w1lieNNGATNQ==}
|
||||
engines: {node: '>= 8.0.0'}
|
||||
peerDependencies:
|
||||
pg-native: '>=3.0.1'
|
||||
peerDependenciesMeta:
|
||||
pg-native:
|
||||
optional: true
|
||||
|
||||
pgpass@1.0.5:
|
||||
resolution: {integrity: sha512-FdW9r/jQZhSeohs1Z3sI1yxFQNFvMcnmfuj4WBMUTxOrAyLMaTcE1aAMBiTlbMNaXvBCQuVi0R7hd8udDSP7ug==}
|
||||
|
||||
@@ -10642,18 +10673,29 @@ packages:
|
||||
resolution: {integrity: sha512-xy3pmLuQqRBZBXDULy7KbaitYqLcmxigw14Q5sj8QBVLqEwXfeybIKVWiqAXTlcvdvb0+xkOtDbfQMOf4lST1w==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
|
||||
postgres-bytea@3.0.0:
|
||||
resolution: {integrity: sha512-CNd4jim9RFPkObHSjVHlVrxoVQXz7quwNFpz7RY1okNNme49+sVyiTvTRobiLV548Hx/hb1BG+iE7h9493WzFw==}
|
||||
engines: {node: '>= 6'}
|
||||
|
||||
postgres-date@1.0.7:
|
||||
resolution: {integrity: sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
|
||||
postgres-date@2.1.0:
|
||||
resolution: {integrity: sha512-K7Juri8gtgXVcDfZttFKVmhglp7epKb1K4pgrkLxehjqkrgPhfG6OO8LHLkfaqkbpjNRnra018XwAr1yQFWGcA==}
|
||||
engines: {node: '>=12'}
|
||||
|
||||
postgres-interval@1.2.0:
|
||||
resolution: {integrity: sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
|
||||
postgres@3.4.5:
|
||||
resolution: {integrity: sha512-cDWgoah1Gez9rN3H4165peY9qfpEo+SA61oQv65O3cRUE1pOEoJWwddwcqKE8XZYjbblOJlYDlLV4h67HrEVDg==}
|
||||
postgres-interval@3.0.0:
|
||||
resolution: {integrity: sha512-BSNDnbyZCXSxgA+1f5UU2GmwhoI0aU5yMxRGO8CdFEcY2BQF9xm/7MqKnYoM1nJDk8nONNWDk9WeSmePFhQdlw==}
|
||||
engines: {node: '>=12'}
|
||||
|
||||
postgres-range@1.1.4:
|
||||
resolution: {integrity: sha512-i/hbxIE9803Alj/6ytL7UHQxRvZkI9O4Sy+J3HGc4F4oo/2eQAjTSNJ0bfxyse3bH0nuVesCk+3IRLaMtG3H6w==}
|
||||
|
||||
prelude-ls@1.2.1:
|
||||
resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==}
|
||||
engines: {node: '>= 0.8.0'}
|
||||
@@ -15033,12 +15075,6 @@ snapshots:
|
||||
lodash: 4.17.21
|
||||
postcss: 8.4.38
|
||||
|
||||
'@mertasan/tailwindcss-variables@2.7.0(autoprefixer@10.4.16(postcss@8.5.1))(postcss@8.5.1)':
|
||||
dependencies:
|
||||
autoprefixer: 10.4.16(postcss@8.5.1)
|
||||
lodash: 4.17.21
|
||||
postcss: 8.5.1
|
||||
|
||||
'@monaco-editor/loader@1.4.0(monaco-editor@0.33.0)':
|
||||
dependencies:
|
||||
monaco-editor: 0.33.0
|
||||
@@ -17512,7 +17548,7 @@ snapshots:
|
||||
lz-string: 1.5.0
|
||||
pretty-format: 27.5.1
|
||||
|
||||
'@testing-library/jest-dom@6.4.6(@jest/globals@29.7.0(supports-color@8.1.1))(@types/jest@29.5.5)(jest@29.7.0(@types/node@20.12.11)(supports-color@8.1.1))(vitest@3.0.4(@types/node@20.12.11)(@vitest/ui@3.0.4)(jsdom@20.0.3(supports-color@8.1.1))(msw@2.4.11(typescript@5.5.2))(sass@1.72.0)(supports-color@8.1.1)(terser@5.37.0))':
|
||||
'@testing-library/jest-dom@6.4.6(@jest/globals@29.7.0(supports-color@8.1.1))(@types/jest@29.5.5)(jest@29.7.0(@types/node@20.12.11)(supports-color@8.1.1))(vitest@3.0.4)':
|
||||
dependencies:
|
||||
'@adobe/css-tools': 4.4.0
|
||||
'@babel/runtime': 7.24.7
|
||||
@@ -17949,10 +17985,16 @@ snapshots:
|
||||
dependencies:
|
||||
'@types/pg': 8.6.1
|
||||
|
||||
'@types/pg@8.11.11':
|
||||
dependencies:
|
||||
'@types/node': 22.12.0
|
||||
pg-protocol: 1.7.1
|
||||
pg-types: 4.0.2
|
||||
|
||||
'@types/pg@8.6.1':
|
||||
dependencies:
|
||||
'@types/node': 22.12.0
|
||||
pg-protocol: 1.7.0
|
||||
pg-protocol: 1.7.1
|
||||
pg-types: 2.2.0
|
||||
|
||||
'@types/phoenix@1.6.4': {}
|
||||
@@ -18728,16 +18770,6 @@ snapshots:
|
||||
postcss: 8.4.38
|
||||
postcss-value-parser: 4.2.0
|
||||
|
||||
autoprefixer@10.4.16(postcss@8.5.1):
|
||||
dependencies:
|
||||
browserslist: 4.23.3
|
||||
caniuse-lite: 1.0.30001651
|
||||
fraction.js: 4.3.6
|
||||
normalize-range: 0.1.2
|
||||
picocolors: 1.0.1
|
||||
postcss: 8.5.1
|
||||
postcss-value-parser: 4.2.0
|
||||
|
||||
available-typed-arrays@1.0.7:
|
||||
dependencies:
|
||||
possible-typed-array-names: 1.0.0
|
||||
@@ -20123,7 +20155,7 @@ snapshots:
|
||||
debug: 4.3.7(supports-color@8.1.1)
|
||||
enhanced-resolve: 5.17.1
|
||||
eslint: 8.57.0(supports-color@8.1.1)
|
||||
eslint-module-utils: 2.8.1(@typescript-eslint/parser@7.2.0(eslint@8.57.0(supports-color@8.1.1))(supports-color@8.1.1)(typescript@5.5.2))(eslint-import-resolver-node@0.3.9(supports-color@8.1.1))(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@7.2.0(eslint@8.57.0(supports-color@8.1.1))(supports-color@8.1.1)(typescript@5.5.2))(eslint-import-resolver-node@0.3.9(supports-color@8.1.1))(eslint-plugin-import@2.29.1)(eslint@8.57.0(supports-color@8.1.1))(supports-color@8.1.1))(eslint@8.57.0(supports-color@8.1.1))(supports-color@8.1.1)
|
||||
eslint-module-utils: 2.8.1(@typescript-eslint/parser@7.2.0(eslint@8.57.0(supports-color@8.1.1))(supports-color@8.1.1)(typescript@5.5.2))(eslint-import-resolver-node@0.3.9(supports-color@8.1.1))(eslint-import-resolver-typescript@3.6.1)(eslint@8.57.0(supports-color@8.1.1))(supports-color@8.1.1)
|
||||
eslint-plugin-import: 2.29.1(@typescript-eslint/parser@7.2.0(eslint@8.57.0(supports-color@8.1.1))(supports-color@8.1.1)(typescript@5.5.2))(eslint-import-resolver-typescript@3.6.1)(eslint@8.57.0(supports-color@8.1.1))(supports-color@8.1.1)
|
||||
fast-glob: 3.3.2
|
||||
get-tsconfig: 4.7.2
|
||||
@@ -20135,7 +20167,7 @@ snapshots:
|
||||
- eslint-import-resolver-webpack
|
||||
- supports-color
|
||||
|
||||
eslint-module-utils@2.8.1(@typescript-eslint/parser@7.2.0(eslint@8.57.0(supports-color@8.1.1))(supports-color@8.1.1)(typescript@5.5.2))(eslint-import-resolver-node@0.3.9(supports-color@8.1.1))(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@7.2.0(eslint@8.57.0(supports-color@8.1.1))(supports-color@8.1.1)(typescript@5.5.2))(eslint-import-resolver-node@0.3.9(supports-color@8.1.1))(eslint-plugin-import@2.29.1)(eslint@8.57.0(supports-color@8.1.1))(supports-color@8.1.1))(eslint@8.57.0(supports-color@8.1.1))(supports-color@8.1.1):
|
||||
eslint-module-utils@2.8.1(@typescript-eslint/parser@7.2.0(eslint@8.57.0(supports-color@8.1.1))(supports-color@8.1.1)(typescript@5.5.2))(eslint-import-resolver-node@0.3.9(supports-color@8.1.1))(eslint-import-resolver-typescript@3.6.1)(eslint@8.57.0(supports-color@8.1.1))(supports-color@8.1.1):
|
||||
dependencies:
|
||||
debug: 3.2.7(supports-color@8.1.1)
|
||||
optionalDependencies:
|
||||
@@ -20162,7 +20194,7 @@ snapshots:
|
||||
doctrine: 2.1.0
|
||||
eslint: 8.57.0(supports-color@8.1.1)
|
||||
eslint-import-resolver-node: 0.3.9(supports-color@8.1.1)
|
||||
eslint-module-utils: 2.8.1(@typescript-eslint/parser@7.2.0(eslint@8.57.0(supports-color@8.1.1))(supports-color@8.1.1)(typescript@5.5.2))(eslint-import-resolver-node@0.3.9(supports-color@8.1.1))(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@7.2.0(eslint@8.57.0(supports-color@8.1.1))(supports-color@8.1.1)(typescript@5.5.2))(eslint-import-resolver-node@0.3.9(supports-color@8.1.1))(eslint-plugin-import@2.29.1)(eslint@8.57.0(supports-color@8.1.1))(supports-color@8.1.1))(eslint@8.57.0(supports-color@8.1.1))(supports-color@8.1.1)
|
||||
eslint-module-utils: 2.8.1(@typescript-eslint/parser@7.2.0(eslint@8.57.0(supports-color@8.1.1))(supports-color@8.1.1)(typescript@5.5.2))(eslint-import-resolver-node@0.3.9(supports-color@8.1.1))(eslint-import-resolver-typescript@3.6.1)(eslint@8.57.0(supports-color@8.1.1))(supports-color@8.1.1)
|
||||
hasown: 2.0.2
|
||||
is-core-module: 2.13.1
|
||||
is-glob: 4.0.3
|
||||
@@ -23673,6 +23705,11 @@ snapshots:
|
||||
next: 14.2.21(@babel/core@7.24.7(supports-color@8.1.1))(@opentelemetry/api@1.9.0)(@playwright/test@1.49.1)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(sass@1.72.0)
|
||||
react: 18.2.0
|
||||
|
||||
next-router-mock@0.9.13(next@14.2.21(@babel/core@7.26.7(supports-color@8.1.1))(@opentelemetry/api@1.9.0)(@playwright/test@1.49.1)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(sass@1.72.0))(react@18.2.0):
|
||||
dependencies:
|
||||
next: 14.2.21(@babel/core@7.26.7(supports-color@8.1.1))(@opentelemetry/api@1.9.0)(@playwright/test@1.49.1)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(sass@1.72.0)
|
||||
react: 18.2.0
|
||||
|
||||
next-seo@6.5.0(next@14.2.21(@opentelemetry/api@1.9.0)(@playwright/test@1.49.1)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(sass@1.72.0))(react-dom@18.2.0(react@18.2.0))(react@18.2.0):
|
||||
dependencies:
|
||||
next: 14.2.21(@babel/core@7.24.7(supports-color@8.1.1))(@opentelemetry/api@1.9.0)(@playwright/test@1.49.1)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(sass@1.72.0)
|
||||
@@ -23712,6 +23749,34 @@ snapshots:
|
||||
- '@babel/core'
|
||||
- babel-plugin-macros
|
||||
|
||||
next@14.2.21(@babel/core@7.26.7(supports-color@8.1.1))(@opentelemetry/api@1.9.0)(@playwright/test@1.49.1)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(sass@1.72.0):
|
||||
dependencies:
|
||||
'@next/env': 14.2.21
|
||||
'@swc/helpers': 0.5.5
|
||||
busboy: 1.6.0
|
||||
caniuse-lite: 1.0.30001695
|
||||
graceful-fs: 4.2.11
|
||||
postcss: 8.4.31
|
||||
react: 18.2.0
|
||||
react-dom: 18.2.0(react@18.2.0)
|
||||
styled-jsx: 5.1.1(@babel/core@7.26.7(supports-color@8.1.1))(react@18.2.0)
|
||||
optionalDependencies:
|
||||
'@next/swc-darwin-arm64': 14.2.21
|
||||
'@next/swc-darwin-x64': 14.2.21
|
||||
'@next/swc-linux-arm64-gnu': 14.2.21
|
||||
'@next/swc-linux-arm64-musl': 14.2.21
|
||||
'@next/swc-linux-x64-gnu': 14.2.21
|
||||
'@next/swc-linux-x64-musl': 14.2.21
|
||||
'@next/swc-win32-arm64-msvc': 14.2.21
|
||||
'@next/swc-win32-ia32-msvc': 14.2.21
|
||||
'@next/swc-win32-x64-msvc': 14.2.21
|
||||
'@opentelemetry/api': 1.9.0
|
||||
'@playwright/test': 1.49.1
|
||||
sass: 1.72.0
|
||||
transitivePeerDependencies:
|
||||
- '@babel/core'
|
||||
- babel-plugin-macros
|
||||
|
||||
nice-try@1.0.5: {}
|
||||
|
||||
no-case@3.0.4:
|
||||
@@ -23952,6 +24017,8 @@ snapshots:
|
||||
|
||||
obliterator@2.0.4: {}
|
||||
|
||||
obuf@1.1.2: {}
|
||||
|
||||
on-exit-leak-free@2.1.2: {}
|
||||
|
||||
once@1.4.0:
|
||||
@@ -24182,19 +24249,27 @@ snapshots:
|
||||
|
||||
pg-connection-string@2.6.2: {}
|
||||
|
||||
pg-connection-string@2.7.0: {}
|
||||
|
||||
pg-format@1.0.4: {}
|
||||
|
||||
pg-int8@1.0.1: {}
|
||||
|
||||
pg-minify@1.6.3: {}
|
||||
|
||||
pg-pool@3.6.1(pg@8.11.3):
|
||||
pg-numeric@1.0.2: {}
|
||||
|
||||
pg-pool@3.7.1(pg@8.11.3):
|
||||
dependencies:
|
||||
pg: 8.11.3
|
||||
|
||||
pg-pool@3.7.1(pg@8.13.1):
|
||||
dependencies:
|
||||
pg: 8.13.1
|
||||
|
||||
pg-protocol@1.6.0: {}
|
||||
|
||||
pg-protocol@1.7.0: {}
|
||||
pg-protocol@1.7.1: {}
|
||||
|
||||
pg-types@2.2.0:
|
||||
dependencies:
|
||||
@@ -24204,13 +24279,33 @@ snapshots:
|
||||
postgres-date: 1.0.7
|
||||
postgres-interval: 1.2.0
|
||||
|
||||
pg-types@4.0.2:
|
||||
dependencies:
|
||||
pg-int8: 1.0.1
|
||||
pg-numeric: 1.0.2
|
||||
postgres-array: 3.0.2
|
||||
postgres-bytea: 3.0.0
|
||||
postgres-date: 2.1.0
|
||||
postgres-interval: 3.0.0
|
||||
postgres-range: 1.1.4
|
||||
|
||||
pg@8.11.3:
|
||||
dependencies:
|
||||
buffer-writer: 2.0.0
|
||||
packet-reader: 1.0.0
|
||||
pg-connection-string: 2.6.2
|
||||
pg-pool: 3.6.1(pg@8.11.3)
|
||||
pg-protocol: 1.6.0
|
||||
pg-connection-string: 2.7.0
|
||||
pg-pool: 3.7.1(pg@8.11.3)
|
||||
pg-protocol: 1.7.1
|
||||
pg-types: 2.2.0
|
||||
pgpass: 1.0.5
|
||||
optionalDependencies:
|
||||
pg-cloudflare: 1.1.1
|
||||
|
||||
pg@8.13.1:
|
||||
dependencies:
|
||||
pg-connection-string: 2.7.0
|
||||
pg-pool: 3.7.1(pg@8.13.1)
|
||||
pg-protocol: 1.7.1
|
||||
pg-types: 2.2.0
|
||||
pgpass: 1.0.5
|
||||
optionalDependencies:
|
||||
@@ -24404,13 +24499,21 @@ snapshots:
|
||||
|
||||
postgres-bytea@1.0.0: {}
|
||||
|
||||
postgres-bytea@3.0.0:
|
||||
dependencies:
|
||||
obuf: 1.1.2
|
||||
|
||||
postgres-date@1.0.7: {}
|
||||
|
||||
postgres-date@2.1.0: {}
|
||||
|
||||
postgres-interval@1.2.0:
|
||||
dependencies:
|
||||
xtend: 4.0.2
|
||||
|
||||
postgres@3.4.5: {}
|
||||
postgres-interval@3.0.0: {}
|
||||
|
||||
postgres-range@1.1.4: {}
|
||||
|
||||
prelude-ls@1.2.1: {}
|
||||
|
||||
@@ -26000,6 +26103,13 @@ snapshots:
|
||||
optionalDependencies:
|
||||
'@babel/core': 7.24.7(supports-color@8.1.1)
|
||||
|
||||
styled-jsx@5.1.1(@babel/core@7.26.7(supports-color@8.1.1))(react@18.2.0):
|
||||
dependencies:
|
||||
client-only: 0.0.1
|
||||
react: 18.2.0
|
||||
optionalDependencies:
|
||||
'@babel/core': 7.26.7(supports-color@8.1.1)
|
||||
|
||||
stylis@4.3.1: {}
|
||||
|
||||
sucrase@3.34.0:
|
||||
|
||||
@@ -30,13 +30,18 @@ export default defineConfig({
|
||||
trace: 'on-first-retry',
|
||||
// record a video for failed tests, but only for local testing. We can't store videos on CI atm.
|
||||
video: 'retain-on-failure',
|
||||
launchOptions: {
|
||||
env: {
|
||||
NODE_ENV: 'test',
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
/* Configure projects for major browsers */
|
||||
projects: [
|
||||
{
|
||||
name: 'chromium',
|
||||
use: { ...devices['Desktop Chrome'] },
|
||||
use: { ...devices['Desktop Chrome'], viewport: { width: 1366, height: 768 } },
|
||||
},
|
||||
|
||||
// {
|
||||
|
||||
@@ -0,0 +1,81 @@
|
||||
import { test, expect } from '@playwright/test'
|
||||
|
||||
const LOGS_PAGES = [
|
||||
'API Gateway',
|
||||
'Postgres',
|
||||
'PostgREST',
|
||||
// 'Auth', Wont have logs on first load
|
||||
// 'Storage', Wont have logs on first load
|
||||
//'Realtime' Wont have logs on first load
|
||||
]
|
||||
|
||||
test.describe('Logs', async () => {
|
||||
for (const logPage of LOGS_PAGES) {
|
||||
test.describe(`${logPage} logs page`, () => {
|
||||
test('can navigate to logs page', async ({ page }) => {
|
||||
await page.goto('http://localhost:8082/project/default')
|
||||
await page.locator('a', { hasText: 'Logs' }).click({ timeout: 4000 })
|
||||
await expect(page.getByText('Logs & Analytics')).toBeVisible()
|
||||
|
||||
// Click anywhere on the screen to close the sidebar
|
||||
await page.click('body')
|
||||
|
||||
await page
|
||||
.getByRole('link', { name: logPage, exact: true })
|
||||
.click()
|
||||
.catch((e) => {
|
||||
console.log('🔴 Error clicking', logPage, e)
|
||||
throw e
|
||||
})
|
||||
|
||||
// Wait for and verify the logs table is present
|
||||
const logsTable = page.getByRole('table')
|
||||
await expect(logsTable).toBeVisible()
|
||||
})
|
||||
|
||||
test('shows logs data without errors', async ({ page }) => {
|
||||
// Navigate to page first
|
||||
await page.goto('http://localhost:8082/project/default')
|
||||
await page.locator('a', { hasText: 'Logs' }).click({ timeout: 4000 })
|
||||
await page.click('body')
|
||||
await page.getByRole('link', { name: logPage, exact: true }).click()
|
||||
|
||||
// Wait a bit and check for errors with a longer timeout
|
||||
const error = page.getByText('Error fetching logs')
|
||||
await expect(error).not.toBeVisible({ timeout: 10000 })
|
||||
|
||||
// Check if the logs table has any rows
|
||||
const gridcells = page.getByRole('gridcell')
|
||||
await expect(gridcells.first()).toBeVisible()
|
||||
})
|
||||
|
||||
test('can select and view log details', async ({ page }) => {
|
||||
// Navigate to page first
|
||||
await page.goto('http://localhost:8082/project/default')
|
||||
await page.locator('a', { hasText: 'Logs' }).click({ timeout: 4000 })
|
||||
await page.click('body')
|
||||
await page.getByRole('link', { name: logPage, exact: true }).click()
|
||||
|
||||
const gridcells = page.getByRole('gridcell')
|
||||
|
||||
// Click first row and verify details
|
||||
await gridcells.first().click()
|
||||
const tabPanel = page.getByTestId('log-selection')
|
||||
await expect(tabPanel).toBeVisible({ timeout: 2000 })
|
||||
|
||||
const selectionPanelTimestamp = tabPanel.getByTestId('log-selection-timestamp')
|
||||
await expect(selectionPanelTimestamp).toBeVisible()
|
||||
|
||||
const rawTimestamp = await selectionPanelTimestamp.textContent()
|
||||
const timestamp = rawTimestamp?.replace('timestamp', '')
|
||||
const rowText = await gridcells.first().textContent()
|
||||
expect(rowText).toContain(timestamp)
|
||||
|
||||
// Click second row and verify different content
|
||||
await gridcells.nth(1).click()
|
||||
const tabPanelText2 = await tabPanel.textContent()
|
||||
expect(tabPanelText2).not.toBe(rowText)
|
||||
})
|
||||
})
|
||||
}
|
||||
})
|
||||
@@ -19,7 +19,7 @@ test.describe('Table Editor page', () => {
|
||||
test('should create a new table, view its definition, add new rows, sort and filter', async ({
|
||||
page,
|
||||
}, testInfo) => {
|
||||
const tableName = `${kebabCase(testInfo.title).slice(0, 24)}-${testInfo.retry}-${Math.floor(Math.random() * 100)}`
|
||||
const tableName = `${kebabCase(testInfo.title).slice(0, 24)}-${testInfo.retry}-${Math.floor(Math.random() * 10000)}`
|
||||
|
||||
// The page has been loaded with the table data, we can now interact with the page
|
||||
await page.getByRole('button', { name: 'New table', exact: true }).click()
|
||||
@@ -46,6 +46,14 @@ test.describe('Table Editor page', () => {
|
||||
await page.getByRole('button', { name: 'Save' }).click()
|
||||
await dismissToast(page)
|
||||
|
||||
// hide React Query DevTools if present
|
||||
await page.evaluate(() => {
|
||||
const devtools = document.querySelector('.ReactQueryDevtools')
|
||||
if (devtools) {
|
||||
devtools.remove()
|
||||
}
|
||||
})
|
||||
|
||||
// view its definition
|
||||
await page.getByText('definition').click()
|
||||
await expect(page.locator('div.view-lines')).toContainText(
|
||||
@@ -69,9 +77,17 @@ test.describe('Table Editor page', () => {
|
||||
await page.getByTestId('action-bar-save-row').click()
|
||||
await dismissToast(page)
|
||||
|
||||
// Wait for both rows to be visible in the grid
|
||||
await page.waitForResponse((response) =>
|
||||
response.url().includes('/api/platform/pg-meta/default/query')
|
||||
)
|
||||
await expect(page.getByRole('grid')).toContainText('2')
|
||||
await expect(page.getByRole('grid')).toContainText('100')
|
||||
|
||||
// Make sure we can see both rows in the grid before sorting
|
||||
const rows = page.getByRole('row')
|
||||
await expect(rows).toHaveCount(3) // header row + 2 data rows
|
||||
|
||||
// sort by the a column
|
||||
await page.getByRole('button', { name: 'Sort' }).click()
|
||||
await page.getByTestId('table-editor-pick-column-to-sort-button').click()
|
||||
@@ -82,8 +98,8 @@ test.describe('Table Editor page', () => {
|
||||
await page.locator('#spec-click-target').click()
|
||||
|
||||
// expect the row to be sorted by defaultValueColumn. They're inserted in the order 100, 2
|
||||
await expect(page.locator('div.rdg-row:nth-child(2)')).toContainText('2')
|
||||
await expect(page.locator('div.rdg-row:nth-child(3)')).toContainText('100')
|
||||
await expect(rows.nth(1)).toContainText('2')
|
||||
await expect(rows.nth(2)).toContainText('100')
|
||||
// remove the sorting
|
||||
await page.getByRole('button', { name: 'Sorted by 1 rule' }).click()
|
||||
await page.getByRole('dialog').getByRole('button').nth(1).click()
|
||||
|
||||
Reference in new issue
Block a user