mirror of
https://github.com/supabase/supabase.git
synced 2026-10-06 09:55:06 +03:00
Merge branch 'master' into hi/fix-project-kpsversion
This commit is contained in:
55 files changed
+1947
-2790
No files matched your search
@@ -92,7 +92,7 @@ services:
|
||||
|
||||
realtime:
|
||||
container_name: supabase-realtime
|
||||
image: supabase/realtime:v0.21.0
|
||||
image: supabase/realtime:v0.22.4
|
||||
depends_on:
|
||||
- db
|
||||
restart: unless-stopped
|
||||
@@ -151,7 +151,7 @@ services:
|
||||
|
||||
db:
|
||||
container_name: supabase-db
|
||||
image: supabase/postgres:14.1.0.19
|
||||
image: supabase/postgres:14.1.0.21
|
||||
command: postgres -c config_file=/etc/postgresql/postgresql.conf
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
|
||||
@@ -17,13 +17,19 @@ as $$
|
||||
|
||||
IF server_version >= 14 THEN
|
||||
RETURN jsonb_build_object(
|
||||
'data', null::jsonb,
|
||||
'errors', array['pg_graphql extension is not enabled.']
|
||||
'errors', jsonb_build_array(
|
||||
jsonb_build_object(
|
||||
'message', 'pg_graphql extension is not enabled.'
|
||||
)
|
||||
)
|
||||
);
|
||||
ELSE
|
||||
RETURN jsonb_build_object(
|
||||
'data', null::jsonb,
|
||||
'errors', array['pg_graphql is only available on projects running Postgres 14 onwards.']
|
||||
'errors', jsonb_build_array(
|
||||
jsonb_build_object(
|
||||
'message', 'pg_graphql is only available on projects running Postgres 14 onwards.'
|
||||
)
|
||||
)
|
||||
);
|
||||
END IF;
|
||||
END;
|
||||
@@ -42,13 +48,13 @@ alter default privileges for user supabase_admin in schema graphql_public grant
|
||||
on functions to postgres, anon, authenticated, service_role;
|
||||
|
||||
-- Trigger upon enabling pg_graphql
|
||||
CREATE OR REPLACE FUNCTION extensions.grant_pg_graphql_access()
|
||||
RETURNS event_trigger
|
||||
LANGUAGE plpgsql
|
||||
create or replace function extensions.grant_pg_graphql_access()
|
||||
returns event_trigger
|
||||
language plpgsql
|
||||
AS $func$
|
||||
DECLARE
|
||||
DECLARE
|
||||
func_is_graphql_resolve bool;
|
||||
BEGIN
|
||||
BEGIN
|
||||
func_is_graphql_resolve = (
|
||||
SELECT n.proname = 'resolve'
|
||||
FROM pg_event_trigger_ddl_commands() AS ev
|
||||
@@ -65,23 +71,29 @@ AS $func$
|
||||
alter default privileges in schema graphql grant all on functions to postgres, anon, authenticated, service_role;
|
||||
alter default privileges in schema graphql grant all on sequences to postgres, anon, authenticated, service_role;
|
||||
|
||||
-- Update public wrapper to pass all arguments through to the pg_graphql resolve func
|
||||
create or replace function graphql_public.graphql(
|
||||
"operationName" text default null,
|
||||
query text default null,
|
||||
variables jsonb default null,
|
||||
extensions jsonb default null
|
||||
extensions jsonb default null
|
||||
)
|
||||
returns jsonb
|
||||
language sql
|
||||
as $$
|
||||
SELECT graphql.resolve(query, coalesce(variables, '{}'));
|
||||
select graphql.resolve(
|
||||
query := query,
|
||||
variables := coalesce(variables, '{}'),
|
||||
"operationName" := "operationName",
|
||||
extensions := extensions
|
||||
);
|
||||
$$;
|
||||
|
||||
grant select on graphql.field, graphql.type, graphql.enum_value to postgres, anon, authenticated, service_role;
|
||||
grant execute on function graphql.resolve to postgres, anon, authenticated, service_role;
|
||||
END IF;
|
||||
|
||||
END;
|
||||
END;
|
||||
$func$;
|
||||
|
||||
CREATE EVENT TRIGGER issue_pg_graphql_access ON ddl_command_end WHEN TAG in ('CREATE FUNCTION')
|
||||
@@ -120,13 +132,19 @@ AS $func$
|
||||
|
||||
IF server_version >= 14 THEN
|
||||
RETURN jsonb_build_object(
|
||||
'data', null::jsonb,
|
||||
'errors', array['pg_graphql extension is not enabled.']
|
||||
'errors', jsonb_build_array(
|
||||
jsonb_build_object(
|
||||
'message', 'pg_graphql extension is not enabled.'
|
||||
)
|
||||
)
|
||||
);
|
||||
ELSE
|
||||
RETURN jsonb_build_object(
|
||||
'data', null::jsonb,
|
||||
'errors', array['pg_graphql is only available on projects running Postgres 14 onwards.']
|
||||
'errors', jsonb_build_array(
|
||||
jsonb_build_object(
|
||||
'message', 'pg_graphql is only available on projects running Postgres 14 onwards.'
|
||||
)
|
||||
)
|
||||
);
|
||||
END IF;
|
||||
END;
|
||||
@@ -138,4 +156,7 @@ $func$;
|
||||
|
||||
CREATE EVENT TRIGGER issue_graphql_placeholder ON sql_drop WHEN TAG in ('DROP EXTENSION')
|
||||
EXECUTE PROCEDURE extensions.set_graphql_placeholder();
|
||||
COMMENT ON FUNCTION extensions.set_graphql_placeholder IS 'Reintroduces placeholder function for graphql_public.graphql';
|
||||
COMMENT ON FUNCTION extensions.set_graphql_placeholder IS 'Reintroduces placeholder function for graphql_public.graphql';
|
||||
|
||||
drop extension if exists pg_graphql;
|
||||
create extension if not exists pg_graphql;
|
||||
Generated
+6
-6
@@ -3606,9 +3606,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/minimist": {
|
||||
"version": "1.2.5",
|
||||
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz",
|
||||
"integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw=="
|
||||
"version": "1.2.6",
|
||||
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.6.tgz",
|
||||
"integrity": "sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q=="
|
||||
},
|
||||
"node_modules/mississippi": {
|
||||
"version": "3.0.0",
|
||||
@@ -10124,9 +10124,9 @@
|
||||
}
|
||||
},
|
||||
"minimist": {
|
||||
"version": "1.2.5",
|
||||
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz",
|
||||
"integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw=="
|
||||
"version": "1.2.6",
|
||||
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.6.tgz",
|
||||
"integrity": "sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q=="
|
||||
},
|
||||
"mississippi": {
|
||||
"version": "3.0.0",
|
||||
|
||||
@@ -1988,8 +1988,8 @@ minimatch@^3.0.4:
|
||||
brace-expansion "^1.1.7"
|
||||
|
||||
minimist@^1.2.0, minimist@^1.2.3, minimist@^1.2.5:
|
||||
version "1.2.5"
|
||||
resolved "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz"
|
||||
version "1.2.6"
|
||||
resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.6.tgz#8637a5b759ea0d6e98702cfb3a9283323c93af44"
|
||||
|
||||
mississippi@^3.0.0:
|
||||
version "3.0.0"
|
||||
|
||||
+6
-6
@@ -2202,9 +2202,9 @@
|
||||
"integrity": "sha1-9sAMHAsIIkblxNmd+4x8CDsrWCo="
|
||||
},
|
||||
"node_modules/minimist": {
|
||||
"version": "1.2.5",
|
||||
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz",
|
||||
"integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw=="
|
||||
"version": "1.2.6",
|
||||
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.6.tgz",
|
||||
"integrity": "sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q=="
|
||||
},
|
||||
"node_modules/ms": {
|
||||
"version": "2.0.0",
|
||||
@@ -5152,9 +5152,9 @@
|
||||
"integrity": "sha1-9sAMHAsIIkblxNmd+4x8CDsrWCo="
|
||||
},
|
||||
"minimist": {
|
||||
"version": "1.2.5",
|
||||
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz",
|
||||
"integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw=="
|
||||
"version": "1.2.6",
|
||||
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.6.tgz",
|
||||
"integrity": "sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q=="
|
||||
},
|
||||
"ms": {
|
||||
"version": "2.0.0",
|
||||
|
||||
+3
-3
@@ -648,9 +648,9 @@
|
||||
}
|
||||
},
|
||||
"minimist": {
|
||||
"version": "1.2.5",
|
||||
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz",
|
||||
"integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw=="
|
||||
"version": "1.2.6",
|
||||
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.6.tgz",
|
||||
"integrity": "sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q=="
|
||||
},
|
||||
"ms": {
|
||||
"version": "2.0.0",
|
||||
|
||||
+3
-3
@@ -1733,9 +1733,9 @@
|
||||
}
|
||||
},
|
||||
"minimist": {
|
||||
"version": "1.2.5",
|
||||
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz",
|
||||
"integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw=="
|
||||
"version": "1.2.6",
|
||||
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.6.tgz",
|
||||
"integrity": "sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q=="
|
||||
},
|
||||
"ms": {
|
||||
"version": "2.0.0",
|
||||
|
||||
+6
-6
@@ -1192,9 +1192,9 @@
|
||||
"integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w=="
|
||||
},
|
||||
"node_modules/minimist": {
|
||||
"version": "1.2.5",
|
||||
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz",
|
||||
"integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw=="
|
||||
"version": "1.2.6",
|
||||
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.6.tgz",
|
||||
"integrity": "sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q=="
|
||||
},
|
||||
"node_modules/ms": {
|
||||
"version": "2.0.0",
|
||||
@@ -2486,9 +2486,9 @@
|
||||
"integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w=="
|
||||
},
|
||||
"minimist": {
|
||||
"version": "1.2.5",
|
||||
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz",
|
||||
"integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw=="
|
||||
"version": "1.2.6",
|
||||
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.6.tgz",
|
||||
"integrity": "sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q=="
|
||||
},
|
||||
"ms": {
|
||||
"version": "2.0.0",
|
||||
|
||||
+6
-6
@@ -2364,9 +2364,9 @@
|
||||
"integrity": "sha1-9sAMHAsIIkblxNmd+4x8CDsrWCo="
|
||||
},
|
||||
"node_modules/minimist": {
|
||||
"version": "1.2.5",
|
||||
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz",
|
||||
"integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw=="
|
||||
"version": "1.2.6",
|
||||
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.6.tgz",
|
||||
"integrity": "sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q=="
|
||||
},
|
||||
"node_modules/ms": {
|
||||
"version": "2.0.0",
|
||||
@@ -5460,9 +5460,9 @@
|
||||
"integrity": "sha1-9sAMHAsIIkblxNmd+4x8CDsrWCo="
|
||||
},
|
||||
"minimist": {
|
||||
"version": "1.2.5",
|
||||
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz",
|
||||
"integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw=="
|
||||
"version": "1.2.6",
|
||||
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.6.tgz",
|
||||
"integrity": "sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q=="
|
||||
},
|
||||
"ms": {
|
||||
"version": "2.0.0",
|
||||
|
||||
File diff suppressed because it is too large.
Load diff
+3
-3
@@ -6191,9 +6191,9 @@
|
||||
}
|
||||
},
|
||||
"minimist": {
|
||||
"version": "1.2.5",
|
||||
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz",
|
||||
"integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw=="
|
||||
"version": "1.2.6",
|
||||
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.6.tgz",
|
||||
"integrity": "sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q=="
|
||||
},
|
||||
"minipass": {
|
||||
"version": "3.1.3",
|
||||
|
||||
+6
-6
@@ -13123,9 +13123,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/minimist": {
|
||||
"version": "1.2.5",
|
||||
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz",
|
||||
"integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw=="
|
||||
"version": "1.2.6",
|
||||
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.6.tgz",
|
||||
"integrity": "sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q=="
|
||||
},
|
||||
"node_modules/minipass": {
|
||||
"version": "3.1.3",
|
||||
@@ -31734,9 +31734,9 @@
|
||||
}
|
||||
},
|
||||
"minimist": {
|
||||
"version": "1.2.5",
|
||||
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz",
|
||||
"integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw=="
|
||||
"version": "1.2.6",
|
||||
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.6.tgz",
|
||||
"integrity": "sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q=="
|
||||
},
|
||||
"minipass": {
|
||||
"version": "3.1.3",
|
||||
|
||||
Generated
+6
-6
@@ -3892,9 +3892,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/minimist": {
|
||||
"version": "1.2.5",
|
||||
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz",
|
||||
"integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==",
|
||||
"version": "1.2.6",
|
||||
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.6.tgz",
|
||||
"integrity": "sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/minimist-options": {
|
||||
@@ -8442,9 +8442,9 @@
|
||||
}
|
||||
},
|
||||
"minimist": {
|
||||
"version": "1.2.5",
|
||||
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz",
|
||||
"integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==",
|
||||
"version": "1.2.6",
|
||||
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.6.tgz",
|
||||
"integrity": "sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q==",
|
||||
"dev": true
|
||||
},
|
||||
"minimist-options": {
|
||||
|
||||
+3
-3
@@ -9762,9 +9762,9 @@
|
||||
"dev": true
|
||||
},
|
||||
"moment": {
|
||||
"version": "2.29.1",
|
||||
"resolved": "https://registry.npmjs.org/moment/-/moment-2.29.1.tgz",
|
||||
"integrity": "sha512-kHmoybcPV8Sqy59DwNDY3Jefr64lK/by/da0ViFcuA4DH0vQg5Q6Ze5VimxkfQNSC+Mls/Kx53s7TjP1RhFEDQ==",
|
||||
"version": "2.29.2",
|
||||
"resolved": "https://registry.npmjs.org/moment/-/moment-2.29.2.tgz",
|
||||
"integrity": "sha512-UgzG4rvxYpN15jgCmVJwac49h9ly9NurikMWGPdVxm8GZD6XjkKPxDTjQQ43gtGgnV3X0cAyWDdP2Wexoquifg==",
|
||||
"dev": true
|
||||
},
|
||||
"ms": {
|
||||
|
||||
+6
-6
@@ -9524,9 +9524,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/minimist": {
|
||||
"version": "1.2.5",
|
||||
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz",
|
||||
"integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==",
|
||||
"version": "1.2.6",
|
||||
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.6.tgz",
|
||||
"integrity": "sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/minipass": {
|
||||
@@ -23742,9 +23742,9 @@
|
||||
}
|
||||
},
|
||||
"minimist": {
|
||||
"version": "1.2.5",
|
||||
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz",
|
||||
"integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==",
|
||||
"version": "1.2.6",
|
||||
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.6.tgz",
|
||||
"integrity": "sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q==",
|
||||
"dev": true
|
||||
},
|
||||
"minipass": {
|
||||
|
||||
Generated
+3
-3
@@ -5415,9 +5415,9 @@
|
||||
}
|
||||
},
|
||||
"minimist": {
|
||||
"version": "1.2.5",
|
||||
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz",
|
||||
"integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==",
|
||||
"version": "1.2.6",
|
||||
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.6.tgz",
|
||||
"integrity": "sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q==",
|
||||
"dev": true
|
||||
},
|
||||
"mixin-deep": {
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
14
|
||||
@@ -15,7 +15,30 @@ const RouteValidationWrapper: FC = ({ children }) => {
|
||||
const projectRef = router.query.ref
|
||||
const orgSlug = router.query.slug
|
||||
|
||||
/**
|
||||
* Array of urls/routes that should be ignored
|
||||
*/
|
||||
const excemptUrls: string[] = [
|
||||
// project creation route, allows the page to self determine it's own route, it will redirect to the first org
|
||||
// or prompt the user to create an organaization
|
||||
// this is used by database.dev, usually as /new/new-project
|
||||
'/new/[slug]',
|
||||
]
|
||||
|
||||
/**
|
||||
* Map through all the urls that are excluded
|
||||
* from route validation check
|
||||
*
|
||||
* @returns a boolean
|
||||
*/
|
||||
function isExceptUrl() {
|
||||
return excemptUrls.includes(router?.pathname)
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
// check if current route is excempted from route validation check
|
||||
if (isExceptUrl()) return
|
||||
|
||||
if (orgsInitialized && orgSlug) {
|
||||
// Check validity of organization that user is trying to access
|
||||
const organizations = app.organizations.list()
|
||||
@@ -30,6 +53,9 @@ const RouteValidationWrapper: FC = ({ children }) => {
|
||||
}, [orgsInitialized])
|
||||
|
||||
useEffect(() => {
|
||||
// check if current route is excempted from route validation check
|
||||
if (isExceptUrl()) return
|
||||
|
||||
if (projectsInitialized && projectRef) {
|
||||
// Check validity of project that the user is trying to access
|
||||
const projects = app.projects.list()
|
||||
|
||||
@@ -27,7 +27,13 @@ import { USAGE_COLORS } from 'components/ui/Charts/Charts.constants'
|
||||
import { EndpointResponse, PathsDatum, StatusCodesDatum } from './ChartData.types'
|
||||
|
||||
const CHART_INTERVALS = [
|
||||
{ key: 'minutely', label: '60 minutes', startValue: 1, startUnit: 'hour', format: 'MMM D, h:mma' },
|
||||
{
|
||||
key: 'minutely',
|
||||
label: '60 minutes',
|
||||
startValue: 1,
|
||||
startUnit: 'hour',
|
||||
format: 'MMM D, h:mma',
|
||||
},
|
||||
{ key: 'hourly', label: '24 hours', startValue: 24, startUnit: 'hour', format: 'MMM D, ha' },
|
||||
{ key: 'daily', label: '7 days', startValue: 7, startUnit: 'day', format: 'MMM D' },
|
||||
]
|
||||
@@ -37,7 +43,6 @@ interface Props {
|
||||
|
||||
const ProjectUsage: FC<Props> = ({ project }) => {
|
||||
const logsUsageCodesPaths = useFlag('logsUsageCodesPaths')
|
||||
const logsUsageChartIntervals = useFlag('logsUsageChartIntervals')
|
||||
const [interval, setInterval] = useState<string>('hourly')
|
||||
const router = useRouter()
|
||||
const { ref } = router.query
|
||||
@@ -59,9 +64,7 @@ const ProjectUsage: FC<Props> = ({ project }) => {
|
||||
get
|
||||
)
|
||||
|
||||
const selectedInterval = logsUsageChartIntervals
|
||||
? CHART_INTERVALS.find((i) => i.key === interval) || CHART_INTERVALS[1]
|
||||
: CHART_INTERVALS[2]
|
||||
const selectedInterval = CHART_INTERVALS.find((i) => i.key === interval) || CHART_INTERVALS[1]
|
||||
const startDate = dayjs()
|
||||
.subtract(selectedInterval.startValue, selectedInterval.startUnit)
|
||||
.format(DATE_FORMAT)
|
||||
@@ -85,25 +88,23 @@ const ProjectUsage: FC<Props> = ({ project }) => {
|
||||
return (
|
||||
<div className="mx-6 space-y-6">
|
||||
<div className="flex flex-row items-center gap-2">
|
||||
{logsUsageChartIntervals && (
|
||||
<Dropdown
|
||||
side="bottom"
|
||||
align="start"
|
||||
overlay={
|
||||
<Dropdown.RadioGroup value={interval} onChange={setInterval}>
|
||||
{CHART_INTERVALS.map((i) => (
|
||||
<Dropdown.Radio key={i.key} value={i.key}>
|
||||
{i.label}
|
||||
</Dropdown.Radio>
|
||||
))}
|
||||
</Dropdown.RadioGroup>
|
||||
}
|
||||
>
|
||||
<Button as="span" type="default" iconRight={<IconChevronDown />}>
|
||||
{selectedInterval.label}
|
||||
</Button>
|
||||
</Dropdown>
|
||||
)}
|
||||
<Dropdown
|
||||
side="bottom"
|
||||
align="start"
|
||||
overlay={
|
||||
<Dropdown.RadioGroup value={interval} onChange={setInterval}>
|
||||
{CHART_INTERVALS.map((i) => (
|
||||
<Dropdown.Radio key={i.key} value={i.key}>
|
||||
{i.label}
|
||||
</Dropdown.Radio>
|
||||
))}
|
||||
</Dropdown.RadioGroup>
|
||||
}
|
||||
>
|
||||
<Button as="span" type="default" iconRight={<IconChevronDown />}>
|
||||
{selectedInterval.label}
|
||||
</Button>
|
||||
</Dropdown>
|
||||
<span className="text-scale-1000 text-xs">
|
||||
Statistics for past {selectedInterval.label}
|
||||
</span>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { LogTemplate } from '.'
|
||||
import { FilterTableSet, LogTemplate } from '.'
|
||||
|
||||
export const TEMPLATES: LogTemplate[] = [
|
||||
{
|
||||
@@ -185,58 +185,21 @@ export const LOG_TYPE_LABEL_MAPPING: { [k: string]: string } = {
|
||||
database: 'Database',
|
||||
}
|
||||
|
||||
export const genDefaultQuery = (table: LogsTableName, where: string | undefined) => {
|
||||
switch (table) {
|
||||
case 'edge_logs':
|
||||
return `select id, timestamp, event_message, metadata, request, response, request.method, request.path, response.status_code
|
||||
from ${table}
|
||||
cross join unnest(metadata) as m
|
||||
cross join unnest(m.request) as request
|
||||
cross join unnest(m.response) as response
|
||||
${where}
|
||||
limit 100
|
||||
`
|
||||
break
|
||||
|
||||
case 'postgres_logs':
|
||||
return `select postgres_logs.timestamp, id, event_message, metadata, metadataparsed.error_severity from ${table}
|
||||
cross join unnest(metadata) as m
|
||||
cross join unnest(m.parsed) as metadataparsed
|
||||
${where}
|
||||
limit 100
|
||||
`
|
||||
break
|
||||
|
||||
case 'function_logs':
|
||||
return `select id, ${table}.timestamp, event_message, metadata.event_type, metadata.function_id, metadata.level, metadata from ${table}
|
||||
cross join unnest(metadata) as metadata
|
||||
${where}
|
||||
limit 100
|
||||
`
|
||||
break
|
||||
|
||||
case 'function_edge_logs':
|
||||
return `select id, ${table}.timestamp, event_message, response.status_code, response, request, request.method, m.function_id, m.execution_time_ms, m.deployment_id, m.version from ${table}
|
||||
cross join unnest(metadata) as m
|
||||
cross join unnest(m.response) as response
|
||||
cross join unnest(m.request) as request
|
||||
${where}
|
||||
limit 100
|
||||
`
|
||||
|
||||
default:
|
||||
return ""
|
||||
break
|
||||
}
|
||||
const _SQL_FILTER_COMMON = {
|
||||
'search_query': (value: string) => `regexp_contains(event_message, '${value}')`
|
||||
}
|
||||
|
||||
export const SQL_FILTER_TEMPLATES: any = {
|
||||
postgres_logs: {
|
||||
..._SQL_FILTER_COMMON,
|
||||
'severity.error': `metadataParsed.error_severity in ('ERROR', 'FATAL', 'PANIC')`,
|
||||
'severity.noError': `metadataParsed.error_severity not in ('ERROR', 'FATAL', 'PANIC')`,
|
||||
'severity.log': `metadataParsed.error_severity = 'LOG'`,
|
||||
},
|
||||
edge_logs: {
|
||||
..._SQL_FILTER_COMMON,
|
||||
'status_code.error': `response.status_code between 500 and 599`,
|
||||
'status_code.success': `response.status_code between 200 and 299`,
|
||||
'status_code.warning': `response.status_code between 400 and 499`,
|
||||
@@ -252,11 +215,13 @@ export const SQL_FILTER_TEMPLATES: any = {
|
||||
'method.options': `request.method = 'OPTIONS'`,
|
||||
},
|
||||
function_edge_logs: {
|
||||
..._SQL_FILTER_COMMON,
|
||||
'status_code.error': `response.status_code between 500 and 599`,
|
||||
'status_code.success': `response.status_code between 200 and 299`,
|
||||
'status_code.warning': `response.status_code between 400 and 499`,
|
||||
},
|
||||
function_logs: {
|
||||
..._SQL_FILTER_COMMON,
|
||||
'severity.error': `metadata.level = 'error'`,
|
||||
'severity.notError': `metadata.level != 'error'`,
|
||||
'severity.log': `metadata.level = 'log'`,
|
||||
@@ -265,12 +230,6 @@ export const SQL_FILTER_TEMPLATES: any = {
|
||||
},
|
||||
}
|
||||
|
||||
// export const genDefaultQuery = (table: string, where: string = ''): string => `SELECT
|
||||
// id, timestamp, event_message, metadata
|
||||
// FROM
|
||||
// ${table}${where ? ' WHERE\n ' + where : ''}
|
||||
// LIMIT 100
|
||||
// `
|
||||
// export const cleanQuery = (str: string) => str.replaceAll(/\n/g, ' ')
|
||||
export const cleanQuery = (str: string) => str.replace(/\n/g, ' ')
|
||||
// .replace(/\n.*\-\-.*(\n)?$?/, "")
|
||||
@@ -309,23 +268,7 @@ export const genQueryParams = (params: { [k: string]: string }) => {
|
||||
const qs = new URLSearchParams(params).toString()
|
||||
return qs
|
||||
}
|
||||
|
||||
interface Filter {
|
||||
label: string
|
||||
key: string
|
||||
options: {
|
||||
key: string
|
||||
label: string
|
||||
description?: string
|
||||
}[]
|
||||
}
|
||||
type FilterOptions = {
|
||||
[table: string]: {
|
||||
[filterName: string]: Filter
|
||||
}
|
||||
}
|
||||
|
||||
export const FILTER_OPTIONS: FilterOptions = {
|
||||
export const FILTER_OPTIONS: FilterTableSet = {
|
||||
// Postgres logs
|
||||
postgres_logs: {
|
||||
severity: {
|
||||
|
||||
@@ -1,141 +0,0 @@
|
||||
import { Button, Checkbox, Form, Popover } from '@supabase/ui'
|
||||
import { pluckObjectFields } from 'lib/helpers'
|
||||
import { useState } from 'react'
|
||||
|
||||
export const LogsFilter = ({ options, filtersState, dispatchFilters, classes }: any) => {
|
||||
const [open, setOpen] = useState(false)
|
||||
|
||||
function handleReset() {
|
||||
// console.log('handleReset ran!')
|
||||
const empty = {
|
||||
[options.key]: [],
|
||||
}
|
||||
|
||||
dispatchFilters({ ...empty })
|
||||
setOpen(!open)
|
||||
}
|
||||
|
||||
// console.log('filters', filters)
|
||||
|
||||
// console.log('item in filter', options)
|
||||
// console.log('options.key', options.key)
|
||||
|
||||
const filteredKey = filtersState[options.key]
|
||||
|
||||
// loop through options in saved filters
|
||||
// and check if any options are active
|
||||
function checkIsActive() {
|
||||
let active = false
|
||||
options.options.map((x: any) => {
|
||||
if (filteredKey && filteredKey.includes(x.key)) {
|
||||
active = true
|
||||
}
|
||||
})
|
||||
return active
|
||||
}
|
||||
|
||||
const isActive = checkIsActive()
|
||||
|
||||
function initialValues() {
|
||||
let compiledOptions = {}
|
||||
|
||||
options.options.map((x: any) => {
|
||||
compiledOptions = {
|
||||
...compiledOptions,
|
||||
[x.key]: filteredKey && filteredKey.includes(x.key) ? true : false,
|
||||
}
|
||||
})
|
||||
|
||||
return compiledOptions
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex items-center">
|
||||
<Popover
|
||||
open={open}
|
||||
align="end"
|
||||
size="medium"
|
||||
onOpenChange={() => setOpen(!open)}
|
||||
overlay={
|
||||
<>
|
||||
<Form
|
||||
initialValues={initialValues()}
|
||||
onSubmit={(values: any, { setSubmitting, handleReset }: any) => {
|
||||
setSubmitting(true)
|
||||
setOpen(false)
|
||||
|
||||
// console.log('values in Logs.filter submit', values)
|
||||
|
||||
const payload: any = {
|
||||
[options.key]: [],
|
||||
}
|
||||
|
||||
Object.keys(values).map((filterKey) => {
|
||||
if (values[filterKey]) {
|
||||
payload[options.key].push(filterKey)
|
||||
}
|
||||
})
|
||||
|
||||
console.log('payload', payload)
|
||||
// console.log('submit form ran')
|
||||
dispatchFilters({ ...payload })
|
||||
// handleReset()
|
||||
}}
|
||||
>
|
||||
{({ isSubmitting }: { isSubmitting: boolean }) => (
|
||||
<>
|
||||
<div className="py-6 space-y-4">
|
||||
{options.options.map((x: any, i: number) => {
|
||||
return (
|
||||
<>
|
||||
<Checkbox
|
||||
id={x.key}
|
||||
className="px-3"
|
||||
label={x.label}
|
||||
description={x.description}
|
||||
size="medium"
|
||||
/>
|
||||
{i !== options.options.length - 1 && <Popover.Seperator />}
|
||||
</>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
<div
|
||||
className="flex items-center justify-end gap-2
|
||||
bg-scale-200 dark:bg-scale-400 py-2 px-3
|
||||
border-t border-scale-400 dark:border-scale-500
|
||||
"
|
||||
>
|
||||
<Button
|
||||
size="tiny"
|
||||
type="default"
|
||||
onClick={() => handleReset()}
|
||||
htmlType="button"
|
||||
>
|
||||
Clear
|
||||
</Button>
|
||||
<Button loading={isSubmitting} type="primary" htmlType="submit">
|
||||
Save
|
||||
</Button>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</Form>
|
||||
</>
|
||||
}
|
||||
portalled
|
||||
showClose
|
||||
side="bottom"
|
||||
>
|
||||
<Button
|
||||
as="span"
|
||||
type={isActive ? 'secondary' : 'default'}
|
||||
onClick={() => setOpen(!open)}
|
||||
className={classes}
|
||||
>
|
||||
{options.label}
|
||||
</Button>
|
||||
</Popover>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,78 +0,0 @@
|
||||
import { LogsTableName, SQL_FILTER_TEMPLATES } from './Logs.constants'
|
||||
import { FilterObject, Override } from './Logs.types'
|
||||
|
||||
export function filterSqlWhereBuilder(
|
||||
filters: FilterObject | any,
|
||||
table: LogsTableName,
|
||||
override?: Override,
|
||||
search?: string
|
||||
) {
|
||||
// remove any filter arrays that are empty
|
||||
const filtersSanitized: any = Object.values(filters).filter((x: any) => x && x.length > 0)
|
||||
|
||||
const keys = Object.keys(filters)
|
||||
|
||||
const hasOverride = override && override.value && override.key
|
||||
// build array of where statements
|
||||
// first item is `where` if there are any filters
|
||||
let whereArray: string[] =
|
||||
(filtersSanitized.length > 0 || hasOverride) && !search ? ['where'] : search ? ['and'] : []
|
||||
|
||||
if (filtersSanitized.length === 0 && !hasOverride) return whereArray
|
||||
|
||||
if (hasOverride) {
|
||||
// count = +1
|
||||
const sql = `(${override?.key}='${override?.value}')`
|
||||
whereArray.push(sql)
|
||||
if (filtersSanitized.length > 0) whereArray.push('and')
|
||||
}
|
||||
|
||||
if (filtersSanitized.length === 0) return whereArray
|
||||
|
||||
keys.map((x: string, i) => {
|
||||
let count = 0
|
||||
// do not parse empty key
|
||||
if (!x) return
|
||||
|
||||
const filterKeyArray: string[] = []
|
||||
|
||||
filters[x].map((value: string, i: number) => {
|
||||
// first line should be WHERE
|
||||
const last = i === filters[x].length - 1
|
||||
|
||||
if (count === 0) {
|
||||
count = +1
|
||||
// the first argument begins with a `(`
|
||||
// if this is also the last argument, it is closed with a `)`
|
||||
const sql = `( ${SQL_FILTER_TEMPLATES[table][`${x}.${value}`]}${last ? ' ) ' : ''}`
|
||||
filterKeyArray.push(sql)
|
||||
return sql
|
||||
} else {
|
||||
count = +1
|
||||
// remaining lines use `or` as standard
|
||||
// if this is also the last argument, it is closed with a `)`
|
||||
const sql = ` or ${SQL_FILTER_TEMPLATES[table][`${x}.${value}`]}${last ? ' ) ' : ''}`
|
||||
filterKeyArray.push(sql)
|
||||
return sql
|
||||
}
|
||||
})
|
||||
|
||||
whereArray.push(...filterKeyArray)
|
||||
|
||||
// if there are multiple filters in an 'and' sequence
|
||||
// then `and` is inserted between them
|
||||
if (filtersSanitized.length > 0 && i < filtersSanitized.length - 1) {
|
||||
whereArray.push(' and ')
|
||||
}
|
||||
|
||||
return filterKeyArray
|
||||
})
|
||||
return whereArray
|
||||
}
|
||||
|
||||
export function filterReducer(state: FilterObject, action: any) {
|
||||
const oldState = { ...state }
|
||||
const newState = { ...oldState, ...action }
|
||||
|
||||
return newState
|
||||
}
|
||||
@@ -65,6 +65,28 @@ export interface FilterObject {
|
||||
te?: string
|
||||
}
|
||||
|
||||
|
||||
export interface FilterSet {
|
||||
label: string
|
||||
key: string
|
||||
options: FilterOption[]
|
||||
}
|
||||
export interface FilterOption {
|
||||
key: string
|
||||
label: string
|
||||
description?: string
|
||||
}
|
||||
|
||||
export type FilterTableSet = {
|
||||
[table: string]: {
|
||||
[filterName: string]: FilterSet
|
||||
}
|
||||
}
|
||||
|
||||
export interface Filters {
|
||||
[key: string]: string | string[] | boolean | undefined | Filters
|
||||
}
|
||||
|
||||
export type Override = {
|
||||
key: string
|
||||
value: string | string[] | undefined
|
||||
|
||||
@@ -0,0 +1,135 @@
|
||||
import { get } from "lodash"
|
||||
import { Filters, LogsTableName, SQL_FILTER_TEMPLATES } from "."
|
||||
|
||||
/**
|
||||
* Recursively retrieve all nested object key paths.
|
||||
*
|
||||
* TODO: move to utils
|
||||
*
|
||||
* @param obj any object
|
||||
* @param parent a string representing the parent key
|
||||
* @returns string[] all dot paths for keys.
|
||||
*/
|
||||
const getDotKeys = (obj: { [k: string]: unknown }, parent?: string): string[] => {
|
||||
const keys = Object.keys(obj).filter(k => obj[k])
|
||||
return keys.flatMap(k => {
|
||||
const currKey = parent ? `${parent}.${k}` : k
|
||||
if (typeof obj[k] === 'object') {
|
||||
return getDotKeys(obj[k] as any, currKey)
|
||||
} else {
|
||||
return [currKey]
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Root keys in the filter object are considered to be AND filters.
|
||||
* Nested keys under a root key are considered to be OR filters.
|
||||
*
|
||||
* For example:
|
||||
* ```
|
||||
* {my_value: 'something', nested: {id: 123, test: 123 }}
|
||||
* ```
|
||||
* This would be converted into `WHERE (my_value = 'something') and (id = 123 or test = 123)
|
||||
*
|
||||
* The template of the filter determines the actual filter statement. If no template is provided, a generic equality statement will be used.
|
||||
* This only applies for root keys of the filter.
|
||||
* For example:
|
||||
* ```
|
||||
* {'my.nested.value': 123}
|
||||
* ```
|
||||
* with no template, it will be converted into `WHERE (my.nested.value = 123)
|
||||
*
|
||||
* @returns a whewhere statement with WHERE clause.
|
||||
*/
|
||||
const _genWhereStatement = (table: LogsTableName, filters: Filters) => {
|
||||
const keys = Object.keys(filters)
|
||||
const filterTemplates = SQL_FILTER_TEMPLATES[table]
|
||||
const _resolveTemplateToStatement = (dotKey: string): string | null => {
|
||||
const template = filterTemplates[dotKey]
|
||||
const value = get(filters, dotKey)
|
||||
if (value !== undefined && typeof template === 'function') {
|
||||
return template(value)
|
||||
} else if (template === undefined) {
|
||||
// resolve unknwon filters (possibly from filter overrides)
|
||||
// no template, set a default
|
||||
if (typeof value === 'string') {
|
||||
return `${dotKey} = '${value}'`
|
||||
} else {
|
||||
return `${dotKey} = ${value}`
|
||||
}
|
||||
} else if (value === undefined && typeof template === 'function') {
|
||||
return null
|
||||
} else {
|
||||
return template
|
||||
}
|
||||
}
|
||||
|
||||
const statement = keys.map(rootKey => {
|
||||
if (typeof filters[rootKey] === 'object') {
|
||||
// join all statements with an OR
|
||||
const nestedStatement = getDotKeys(filters[rootKey] as Filters, rootKey).map(_resolveTemplateToStatement).filter(Boolean).join(" or ")
|
||||
return `(${nestedStatement})`
|
||||
} else {
|
||||
const nestedStatement = _resolveTemplateToStatement(rootKey)
|
||||
if (nestedStatement === null) return null
|
||||
return `(${nestedStatement})`
|
||||
}
|
||||
|
||||
}).filter(Boolean)
|
||||
// join all root statements with AND
|
||||
.join(" and ")
|
||||
|
||||
if (statement) {
|
||||
return 'where ' + statement
|
||||
} else {
|
||||
return ''
|
||||
}
|
||||
}
|
||||
|
||||
export const genDefaultQuery = (table: LogsTableName, filters: Filters) => {
|
||||
const where = _genWhereStatement(table, filters)
|
||||
|
||||
switch (table) {
|
||||
case 'edge_logs':
|
||||
return `select id, timestamp, event_message, metadata, request, response, request.method, request.path, response.status_code
|
||||
from ${table}
|
||||
cross join unnest(metadata) as m
|
||||
cross join unnest(m.request) as request
|
||||
cross join unnest(m.response) as response
|
||||
${where}
|
||||
limit 100
|
||||
`
|
||||
break
|
||||
|
||||
case 'postgres_logs':
|
||||
return `select postgres_logs.timestamp, id, event_message, metadata, metadataparsed.error_severity from ${table}
|
||||
cross join unnest(metadata) as m
|
||||
cross join unnest(m.parsed) as metadataparsed
|
||||
${where}
|
||||
limit 100
|
||||
`
|
||||
break
|
||||
|
||||
case 'function_logs':
|
||||
return `select id, ${table}.timestamp, event_message, metadata.event_type, metadata.function_id, metadata.level, metadata from ${table}
|
||||
cross join unnest(metadata) as metadata
|
||||
${where}
|
||||
limit 100
|
||||
`
|
||||
break
|
||||
|
||||
case 'function_edge_logs':
|
||||
return `select id, ${table}.timestamp, event_message, response.status_code, response, request, request.method, m.function_id, m.execution_time_ms, m.deployment_id, m.version from ${table}
|
||||
cross join unnest(metadata) as m
|
||||
cross join unnest(m.response) as response
|
||||
cross join unnest(m.request) as request
|
||||
${where}
|
||||
limit 100
|
||||
`
|
||||
|
||||
default:
|
||||
return ""
|
||||
break
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
import { Button, Checkbox, Form, Popover } from '@supabase/ui'
|
||||
import React, { useState } from 'react'
|
||||
import { Filters, FilterSet } from '.'
|
||||
|
||||
interface Props {
|
||||
options: FilterSet
|
||||
filters: Filters
|
||||
onFiltersChange: (filters: Filters) => void
|
||||
buttonClassName: string
|
||||
}
|
||||
|
||||
const LogsFilterPopover: React.FC<Props> = ({
|
||||
options,
|
||||
filters,
|
||||
onFiltersChange,
|
||||
buttonClassName,
|
||||
}) => {
|
||||
const [open, setOpen] = useState(false)
|
||||
|
||||
const handleReset = () => {
|
||||
onFiltersChange({})
|
||||
setOpen(false)
|
||||
}
|
||||
const handleToggle = () => setOpen(!open)
|
||||
const checkIsActive = () => {
|
||||
const filter = filters[options.key]
|
||||
if (typeof filter === 'object' && Object.values(filter).some(Boolean)) {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
return (
|
||||
<Popover
|
||||
className="flex items-center"
|
||||
open={open}
|
||||
align="end"
|
||||
size="medium"
|
||||
onOpenChange={handleToggle}
|
||||
overlay={
|
||||
<Form
|
||||
className="w-full"
|
||||
initialValues={filters}
|
||||
onSubmit={(values, { setSubmitting }) => {
|
||||
setSubmitting(true)
|
||||
onFiltersChange(values)
|
||||
setOpen(false)
|
||||
}}
|
||||
>
|
||||
{({ isSubmitting }: { isSubmitting: boolean }) => (
|
||||
<>
|
||||
<div className="py-6 space-y-4">
|
||||
{options.options.map((x, i: number) => (
|
||||
<React.Fragment key={x.key}>
|
||||
<Checkbox
|
||||
value="true"
|
||||
id={`${options.key}.${x.key}`}
|
||||
className="px-3"
|
||||
label={x.label}
|
||||
description={x.description}
|
||||
size="medium"
|
||||
defaultChecked={(filters?.[options.key] as Filters)?.[x.key] as boolean}
|
||||
/>
|
||||
{i !== options.options.length - 1 && <Popover.Seperator />}
|
||||
</React.Fragment>
|
||||
))}
|
||||
</div>
|
||||
<div className="flex items-center justify-end gap-2 bg-scale-200 dark:bg-scale-400 py-2 px-3 border-t border-scale-400 dark:border-scale-500 ">
|
||||
<Button size="tiny" type="default" onClick={handleReset} htmlType="button">
|
||||
Clear
|
||||
</Button>
|
||||
<Button loading={isSubmitting} type="primary" htmlType="submit">
|
||||
Save
|
||||
</Button>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</Form>
|
||||
}
|
||||
portalled
|
||||
showClose
|
||||
side="bottom"
|
||||
>
|
||||
<Button
|
||||
as="span"
|
||||
type={checkIsActive() ? 'secondary' : 'default'}
|
||||
onClick={handleToggle}
|
||||
className={buttonClassName}
|
||||
>
|
||||
{options.label}
|
||||
</Button>
|
||||
</Popover>
|
||||
)
|
||||
}
|
||||
|
||||
export default LogsFilterPopover
|
||||
@@ -2,21 +2,23 @@ import { Tabs } from '@supabase/ui'
|
||||
import { useFlag } from 'hooks'
|
||||
import { useRouter } from 'next/router'
|
||||
|
||||
interface TabItem {
|
||||
label: string
|
||||
id: string
|
||||
}
|
||||
const LogsNavigation = () => {
|
||||
const router = useRouter()
|
||||
const activeRoute = router.pathname.split('/')[4]
|
||||
const { ref } = router.query
|
||||
|
||||
const indexRoute = 'query'
|
||||
const logsSourcesPage = useFlag('logsSourcesPage')
|
||||
const logsSavedQueries = useFlag('logsSavedQueries')
|
||||
|
||||
const tabs = [
|
||||
{ id: 'query', label: 'Query' },
|
||||
logsSourcesPage ? { id: 'sources', label: 'Sources' } : null,
|
||||
logsSavedQueries ? { id: 'saved', label: 'Saved' } : null,
|
||||
{ id: 'templates', label: 'Templates' },
|
||||
].filter(Boolean)
|
||||
].filter(Boolean) as TabItem[]
|
||||
|
||||
return (
|
||||
<Tabs
|
||||
@@ -28,9 +30,8 @@ const LogsNavigation = () => {
|
||||
router.push(`/project/${ref}/logs-explorer/${e === indexRoute ? '' : e}`)
|
||||
}}
|
||||
>
|
||||
{/* TODO: type properly */}
|
||||
{(tabs as any[]).map(({ id, label }) => (
|
||||
<Tabs.Panel id={id} label={label} />
|
||||
{tabs.map((item) => (
|
||||
<Tabs.Panel key={item.id} id={item.id} label={item.label} />
|
||||
))}
|
||||
</Tabs>
|
||||
)
|
||||
|
||||
@@ -1,14 +1,6 @@
|
||||
import React, { useEffect, useReducer, useState } from 'react'
|
||||
import { useRouter } from 'next/router'
|
||||
import {
|
||||
Typography,
|
||||
IconLoader,
|
||||
IconAlertCircle,
|
||||
IconRewind,
|
||||
Button,
|
||||
Card,
|
||||
Input,
|
||||
} from '@supabase/ui'
|
||||
import { Typography, IconAlertCircle, IconRewind, Button, Card, Input } from '@supabase/ui'
|
||||
|
||||
import {
|
||||
LogTable,
|
||||
@@ -16,10 +8,10 @@ import {
|
||||
TEMPLATES,
|
||||
LogSearchCallback,
|
||||
LogsTableName,
|
||||
filterSqlWhereBuilder,
|
||||
filterReducer,
|
||||
QueryType,
|
||||
LogEventChart,
|
||||
FilterObject,
|
||||
Filters,
|
||||
} from 'components/interfaces/Settings/Logs'
|
||||
import dayjs from 'dayjs'
|
||||
import useLogsPreview from 'hooks/analytics/useLogsPreview'
|
||||
@@ -27,7 +19,6 @@ import PreviewFilterPanel from 'components/interfaces/Settings/Logs/PreviewFilte
|
||||
|
||||
import { LOGS_TABLES } from './Logs.constants'
|
||||
import { Override } from './Logs.types'
|
||||
import ShimmeringLoader from 'components/ui/ShimmeringLoader'
|
||||
import ShimmerLine from 'components/ui/ShimmerLine'
|
||||
import LoadingOpacity from 'components/ui/LoadingOpacity'
|
||||
|
||||
@@ -44,14 +35,14 @@ import LoadingOpacity from 'components/ui/LoadingOpacity'
|
||||
interface Props {
|
||||
projectRef: string
|
||||
queryType: QueryType
|
||||
override?: Override
|
||||
filterOverride?: Filters
|
||||
condensedLayout?: boolean
|
||||
tableName?: LogsTableName
|
||||
}
|
||||
export const LogsPreviewer: React.FC<Props> = ({
|
||||
projectRef,
|
||||
queryType,
|
||||
override,
|
||||
filterOverride,
|
||||
condensedLayout = false,
|
||||
tableName,
|
||||
}) => {
|
||||
@@ -59,22 +50,12 @@ export const LogsPreviewer: React.FC<Props> = ({
|
||||
const { s, te, ts } = router.query
|
||||
const [showChart, setShowChart] = useState(true)
|
||||
|
||||
const [whereFilters, dispatchWhereFilters] = useReducer(filterReducer, {})
|
||||
|
||||
const table = !tableName ? LOGS_TABLES[queryType] : tableName
|
||||
|
||||
const [
|
||||
{ error, logData, params, newCount, filters, isLoading, oldestTimestamp },
|
||||
{ loadOlder, setFilters, refresh, setTo, setFrom },
|
||||
] = useLogsPreview(projectRef as string, table, {
|
||||
initialFilters: { search_query: s as string },
|
||||
whereStatementFactory: (filterObj) => `${
|
||||
filterObj.search_query
|
||||
? `where REGEXP_CONTAINS(event_message, '${filterObj.search_query}')`
|
||||
: ''
|
||||
}
|
||||
${filterSqlWhereBuilder(whereFilters, table, override, filterObj.search_query).join('\n')}`,
|
||||
})
|
||||
] = useLogsPreview(projectRef as string, table, filterOverride)
|
||||
|
||||
useEffect(() => {
|
||||
setFilters((prev) => ({ ...prev, search_query: s as string }))
|
||||
@@ -91,14 +72,9 @@ export const LogsPreviewer: React.FC<Props> = ({
|
||||
}, [s, te, ts])
|
||||
|
||||
const onSelectTemplate = (template: LogTemplate) => {
|
||||
setFilters((prev) => ({ ...prev, search_query: template.searchString }))
|
||||
setFilters((prev: any) => ({ ...prev, search_query: template.searchString }))
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
// runs when any of the filters change
|
||||
handleRefresh()
|
||||
}, [whereFilters])
|
||||
|
||||
const handleRefresh = () => {
|
||||
refresh()
|
||||
router.push({
|
||||
@@ -135,7 +111,6 @@ export const LogsPreviewer: React.FC<Props> = ({
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="h-full flex flex-col flex-grow">
|
||||
<PreviewFilterPanel
|
||||
@@ -147,7 +122,7 @@ export const LogsPreviewer: React.FC<Props> = ({
|
||||
)}
|
||||
onRefresh={handleRefresh}
|
||||
onSearch={handleSearch}
|
||||
defaultSearchValue={filters.search_query}
|
||||
defaultSearchValue={filters.search_query as string}
|
||||
defaultToValue={
|
||||
params.timestamp_end ? dayjs(Number(params.timestamp_end) / 1000).toISOString() : ''
|
||||
}
|
||||
@@ -162,8 +137,8 @@ export const LogsPreviewer: React.FC<Props> = ({
|
||||
router.push(`/project/${projectRef}/logs-explorer?q=${encodeURIComponent(params.rawSql)}`)
|
||||
}}
|
||||
onSelectTemplate={onSelectTemplate}
|
||||
dispatchWhereFilters={dispatchWhereFilters}
|
||||
whereFilters={whereFilters}
|
||||
filters={filters}
|
||||
onFiltersChange={setFilters}
|
||||
table={table}
|
||||
condensedLayout={condensedLayout}
|
||||
isShowingEventChart={showChart}
|
||||
@@ -182,7 +157,7 @@ export const LogsPreviewer: React.FC<Props> = ({
|
||||
<LogEventChart
|
||||
data={!isLoading ? logData : undefined}
|
||||
onBarClick={(timestampMicro) => {
|
||||
handleSearch({ query: filters.search_query, toMicro: timestampMicro })
|
||||
handleSearch({ query: filters.search_query as string, toMicro: timestampMicro })
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -9,11 +9,11 @@ import {
|
||||
IconEye,
|
||||
IconEyeOff,
|
||||
} from '@supabase/ui'
|
||||
import { LogSearchCallback, LogTemplate } from '.'
|
||||
import { Filters, LogSearchCallback, LogTemplate } from '.'
|
||||
import dayjs from 'dayjs'
|
||||
import utc from 'dayjs/plugin/utc'
|
||||
import { FILTER_OPTIONS, LogsTableName } from './Logs.constants'
|
||||
import { LogsFilter } from './Logs.filter'
|
||||
import LogsFilterPopover from './LogsFilterPopover'
|
||||
import DatePickers from './Logs.DatePickers'
|
||||
import CSVButton from 'components/ui/CSVButton'
|
||||
|
||||
@@ -28,13 +28,13 @@ interface Props {
|
||||
onSearch?: LogSearchCallback
|
||||
onExploreClick?: () => void
|
||||
onSelectTemplate: (template: LogTemplate) => void
|
||||
dispatchWhereFilters: (x: any) => void
|
||||
whereFilters: any
|
||||
table: LogsTableName
|
||||
condensedLayout: Boolean
|
||||
isShowingEventChart: boolean
|
||||
onToggleEventChart: () => void
|
||||
csvData?: unknown[]
|
||||
onFiltersChange: (filters: Filters) => void
|
||||
filters: Filters
|
||||
}
|
||||
|
||||
dayjs.extend(utc)
|
||||
@@ -51,13 +51,13 @@ const PreviewFilterPanel: FC<Props> = ({
|
||||
defaultToValue = '',
|
||||
defaultFromValue = '',
|
||||
onExploreClick,
|
||||
dispatchWhereFilters,
|
||||
whereFilters: filters,
|
||||
table,
|
||||
condensedLayout,
|
||||
isShowingEventChart,
|
||||
onToggleEventChart,
|
||||
csvData,
|
||||
onFiltersChange,
|
||||
filters,
|
||||
table,
|
||||
}) => {
|
||||
const [search, setSearch] = useState('')
|
||||
|
||||
@@ -174,12 +174,12 @@ const PreviewFilterPanel: FC<Props> = ({
|
||||
}
|
||||
|
||||
return (
|
||||
<LogsFilter
|
||||
classes={classes.join(' ')}
|
||||
<LogsFilterPopover
|
||||
buttonClassName={classes.join(' ')}
|
||||
key={`${x.key}-filter`}
|
||||
options={x}
|
||||
dispatchFilters={dispatchWhereFilters}
|
||||
filtersState={filters}
|
||||
onFiltersChange={onFiltersChange}
|
||||
filters={filters}
|
||||
/>
|
||||
)
|
||||
})}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
export * from './Logs.types'
|
||||
export * from './Logs.constants'
|
||||
export * from './Logs.helpers'
|
||||
export * from './Logs.filter'
|
||||
export * from './Logs.utils'
|
||||
export { default as LogsSavedQueriesItem } from './Logs.SavedQueriesItem'
|
||||
export { default as LogsQueryPanel } from './LogsQueryPanel'
|
||||
export { default as LogTable } from './LogTable'
|
||||
export { default as LogEventChart } from './LogEventChart'
|
||||
export { default as LogsFilterPopover } from "./LogsFilterPopover"
|
||||
@@ -82,12 +82,11 @@ export const generateProductRoutes = (ref: string, project?: ProjectBase): Route
|
||||
}
|
||||
|
||||
export const generateOtherRoutes = (ref: string, project?: ProjectBase) => {
|
||||
const logsExplorer = useFlag('logsExplorer')
|
||||
const isProjectBuilding = project?.status !== PROJECT_STATUS.ACTIVE_HEALTHY
|
||||
const buildingUrl = `/project/${ref}/building`
|
||||
|
||||
return [
|
||||
...(IS_PLATFORM && logsExplorer
|
||||
...(IS_PLATFORM
|
||||
? [
|
||||
{
|
||||
key: 'logsExplorer',
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import {
|
||||
cleanQuery,
|
||||
Count,
|
||||
Filters,
|
||||
genCountQuery,
|
||||
genDefaultQuery,
|
||||
genQueryParams,
|
||||
@@ -15,50 +16,36 @@ import useSWRInfinite, { SWRInfiniteKeyLoader } from 'swr/infinite'
|
||||
import { API_URL } from 'lib/constants'
|
||||
import { get } from 'lib/common/fetch'
|
||||
|
||||
interface Data<T> {
|
||||
interface Data {
|
||||
logData: LogData[]
|
||||
error: string | Object | null
|
||||
newCount: number
|
||||
isLoading: boolean
|
||||
pageSize: number
|
||||
filters: T
|
||||
filters: Filters
|
||||
params: LogsEndpointParams
|
||||
oldestTimestamp?: string
|
||||
}
|
||||
interface Handlers<T> {
|
||||
interface Handlers {
|
||||
loadOlder: () => void
|
||||
refresh: () => void
|
||||
setFilters: Dispatch<SetStateAction<T>>
|
||||
setFilters: (filters: Filters | ((previous: Filters) => Filters)) => void
|
||||
setFrom: (value: string) => void
|
||||
setTo: (value: string) => void
|
||||
}
|
||||
|
||||
interface Options<Filters> {
|
||||
initialFilters: Filters
|
||||
whereStatementFactory: WhereStatementFactory<Filters>
|
||||
}
|
||||
|
||||
type WhereStatementFactory<T> = (filters: T) => string
|
||||
|
||||
function useLogsPreview<Filters>(
|
||||
function useLogsPreview(
|
||||
projectRef: string,
|
||||
table: LogsTableName,
|
||||
opts?: Partial<Options<Filters>>
|
||||
): [Data<Filters>, Handlers<Filters>] {
|
||||
const options: Options<Filters> = {
|
||||
initialFilters: {} as Filters,
|
||||
whereStatementFactory: () => '',
|
||||
...(opts ?? {}),
|
||||
}
|
||||
|
||||
filterOverride?: Filters
|
||||
): [Data, Handlers] {
|
||||
const [latestRefresh, setLatestRefresh] = useState<string>(new Date().toISOString())
|
||||
|
||||
const [filters, setFilters] = useState<Filters>(options.initialFilters)
|
||||
const [filters, setFilters] = useState<Filters>({ ...filterOverride })
|
||||
|
||||
const [params, setParams] = useState<LogsEndpointParams>({
|
||||
project: projectRef,
|
||||
sql: cleanQuery(genDefaultQuery(table, options.whereStatementFactory(filters))),
|
||||
rawSql: genDefaultQuery(table, options.whereStatementFactory(filters)),
|
||||
sql: '',
|
||||
rawSql: '',
|
||||
period_start: '',
|
||||
period_end: '',
|
||||
timestamp_start: '',
|
||||
@@ -74,6 +61,14 @@ function useLogsPreview<Filters>(
|
||||
// handle url generation for log pagination
|
||||
const getKeyLogs: SWRInfiniteKeyLoader = (_pageIndex: number, prevPageData: Logs) => {
|
||||
let queryParams
|
||||
|
||||
// cancel request if no sql provided
|
||||
if(!params.sql) {
|
||||
// return null to restrict unnecessary requests to api
|
||||
// https://swr.vercel.app/docs/conditional-fetching#conditional
|
||||
return null
|
||||
}
|
||||
|
||||
// if prev page data is 100 items, could possibly have more records that are not yet fetched within this interval
|
||||
if (prevPageData === null) {
|
||||
// reduce interval window limit by using the timestamp of the last log
|
||||
@@ -96,22 +91,36 @@ function useLogsPreview<Filters>(
|
||||
isValidating,
|
||||
size,
|
||||
setSize,
|
||||
} = useSWRInfinite<Logs>(getKeyLogs, get, { revalidateOnFocus: false })
|
||||
} = useSWRInfinite<Logs>(getKeyLogs, get, { revalidateOnFocus: false, dedupingInterval: 3000 })
|
||||
let logData: LogData[] = []
|
||||
|
||||
const countUrl = `${API_URL}/projects/${projectRef}/analytics/endpoints/logs.all?${genQueryParams(
|
||||
{
|
||||
...params,
|
||||
sql: genCountQuery(table),
|
||||
period_start: String(latestRefresh),
|
||||
} as any
|
||||
)}`
|
||||
const countUrl = () => {
|
||||
|
||||
// cancel request if no sql provided
|
||||
if(!params.sql) {
|
||||
// return null to restrict unnecessary requests to api
|
||||
// https://swr.vercel.app/docs/conditional-fetching#conditional
|
||||
return null
|
||||
}
|
||||
|
||||
const { data: countData } = useSWR<Count>(countUrl, get, { refreshInterval: 5000 })
|
||||
return `${API_URL}/projects/${projectRef}/analytics/endpoints/logs.all?${genQueryParams(
|
||||
{
|
||||
...params,
|
||||
sql: genCountQuery(table),
|
||||
period_start: String(latestRefresh),
|
||||
} as any
|
||||
)}`
|
||||
}
|
||||
|
||||
const { data: countData } = useSWR<Count>(countUrl, get, {
|
||||
revalidateOnFocus: false,
|
||||
dedupingInterval: 5000,
|
||||
refreshInterval: 5000,
|
||||
})
|
||||
const newCount = countData?.result?.[0]?.count ?? 0
|
||||
|
||||
const refresh = async () => {
|
||||
const generatedSql = genDefaultQuery(table, options.whereStatementFactory(filters))
|
||||
const generatedSql = genDefaultQuery(table, filters)
|
||||
setParams((prev) => ({ ...prev, sql: cleanQuery(generatedSql), rawSql: generatedSql }))
|
||||
setLatestRefresh(new Date().toISOString())
|
||||
setSize(1)
|
||||
@@ -126,7 +135,19 @@ function useLogsPreview<Filters>(
|
||||
error = response.error
|
||||
}
|
||||
})
|
||||
|
||||
const oldestTimestamp = logData[logData.length - 1]?.timestamp
|
||||
|
||||
const handleSetFilters: Handlers['setFilters'] = (newFilters) => {
|
||||
if (typeof newFilters === 'function') {
|
||||
setFilters((prev) => {
|
||||
const resolved = newFilters(prev)
|
||||
return { ...resolved, ...filterOverride }
|
||||
})
|
||||
} else {
|
||||
setFilters((prev) => ({ ...prev, ...newFilters, ...filterOverride }))
|
||||
}
|
||||
}
|
||||
return [
|
||||
{
|
||||
newCount,
|
||||
@@ -141,7 +162,7 @@ function useLogsPreview<Filters>(
|
||||
{
|
||||
setFrom: (value) => setParams((prev) => ({ ...prev, timestamp_start: value })),
|
||||
setTo: (value) => setParams((prev) => ({ ...prev, timestamp_end: value })),
|
||||
setFilters,
|
||||
setFilters: handleSetFilters,
|
||||
refresh,
|
||||
loadOlder: () => setSize((prev) => prev + 1),
|
||||
},
|
||||
|
||||
Generated
+3
-3
@@ -6143,9 +6143,9 @@
|
||||
}
|
||||
},
|
||||
"@supabase/ui": {
|
||||
"version": "0.37.0-alpha.50",
|
||||
"resolved": "https://registry.npmjs.org/@supabase/ui/-/ui-0.37.0-alpha.50.tgz",
|
||||
"integrity": "sha512-d5jFxIVCRrZ6cHQ6qpuvyaexTmqMnxDWm0hg7vj9D2x5qcmySRCTC42uhpKXz5fKre2TUl38pFT+UpscWw/OyA==",
|
||||
"version": "0.37.0-alpha.51",
|
||||
"resolved": "https://registry.npmjs.org/@supabase/ui/-/ui-0.37.0-alpha.51.tgz",
|
||||
"integrity": "sha512-9N5k6m3iEwR8HYyH7Ekz6EPI2VW2CZZh0TOZuvaKIXSfccgp2w5kcNcRoyGv/t4c7j+8VAyS+H9cRHHSLiwLzQ==",
|
||||
"requires": {
|
||||
"@headlessui/react": "^1.0.0",
|
||||
"@mertasan/tailwindcss-variables": "^2.0.1",
|
||||
|
||||
+1
-1
@@ -26,7 +26,7 @@
|
||||
"@supabase/react-data-grid": "^7.1.0-beta.7",
|
||||
"@supabase/shared-types": "^0.1.15",
|
||||
"@supabase/supabase-js": "^1.29.1",
|
||||
"@supabase/ui": "0.37.0-alpha.50",
|
||||
"@supabase/ui": "0.37.0-alpha.51",
|
||||
"ajv": "^8.6.3",
|
||||
"analytics-node": "^3.5.0",
|
||||
"clipboard": "^2.0.8",
|
||||
|
||||
@@ -16,7 +16,7 @@ export const LogPage: NextPageWithLayout = () => {
|
||||
<LogsPreviewer
|
||||
projectRef={ref as string}
|
||||
queryType={'fn_edge'}
|
||||
override={{ key: 'function_id', value: id }}
|
||||
filterOverride={{function_id: id}}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -16,7 +16,7 @@ export const LogPage: NextPageWithLayout = () => {
|
||||
<LogsPreviewer
|
||||
projectRef={ref as string}
|
||||
queryType={'functions'}
|
||||
override={{ key: 'metadata.function_id', value: id }}
|
||||
filterOverride={{ 'metadata.function_id': id }}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -139,7 +139,6 @@ export const LogsExplorerPage: NextPage = () => {
|
||||
try {
|
||||
const { data: query, error } = await content.create(payload)
|
||||
if (error) throw error
|
||||
console.log(query)
|
||||
setSubmitting(false)
|
||||
setSaveModalOpen(false)
|
||||
toast.success(`Saved "${values.name}" log query`)
|
||||
|
||||
@@ -1,49 +0,0 @@
|
||||
import React from 'react'
|
||||
import { NextPage } from 'next'
|
||||
import { useRouter } from 'next/router'
|
||||
import { observer } from 'mobx-react-lite'
|
||||
import { IconCode, Badge, IconDatabase, Icon, IconCloud } from '@supabase/ui'
|
||||
import { withAuth } from 'hooks'
|
||||
import { LogsTableName, LOGS_SOURCE_DESCRIPTION } from 'components/interfaces/Settings/Logs'
|
||||
import LogsExplorerLayout from 'components/layouts/LogsExplorerLayout/LogsExplorerLayout'
|
||||
import CardButton from 'components/ui/CardButton'
|
||||
|
||||
export const LogsExplorerPage: NextPage = () => {
|
||||
const router = useRouter()
|
||||
const { ref } = router.query
|
||||
// @ts-ignore
|
||||
const sources = Object.values(LogsTableName).sort((a, b) => a - b)
|
||||
return (
|
||||
<LogsExplorerLayout>
|
||||
<div className="grid grid-cols-3 gap-6">
|
||||
{sources.map((source) => (
|
||||
<CardButton
|
||||
title={source}
|
||||
icon={
|
||||
<div
|
||||
className="h-6 w-6 text-scale-100 flex items-center justify-center rounded
|
||||
bg-scale-1200
|
||||
transition-colors
|
||||
duration-400
|
||||
group-hover:bg-brand-900
|
||||
group-hover:text-brand-1200
|
||||
"
|
||||
>
|
||||
<div className="scale-100 group-hover:scale-110">
|
||||
{source === LogsTableName.POSTGRES && <IconDatabase size={12} strokeWidth={2} />}
|
||||
{source === LogsTableName.EDGE && <IconCloud size={12} strokeWidth={2} />}
|
||||
{source === LogsTableName.FUNCTIONS && <IconCode size={12} strokeWidth={2} />}
|
||||
{source === LogsTableName.FN_EDGE && <IconCode size={12} strokeWidth={2} />}
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
linkHref={`/project/${ref}/logs-explorer/sources/${source}`}
|
||||
description={LOGS_SOURCE_DESCRIPTION[source]}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</LogsExplorerLayout>
|
||||
)
|
||||
}
|
||||
|
||||
export default withAuth(observer(LogsExplorerPage))
|
||||
@@ -1,35 +0,0 @@
|
||||
import React from 'react'
|
||||
import { NextPage } from 'next'
|
||||
import { useRouter } from 'next/router'
|
||||
import { observer } from 'mobx-react-lite'
|
||||
import { withAuth } from 'hooks'
|
||||
import { LogsExplorerLayout } from 'components/layouts/'
|
||||
import { LogsTableName } from 'components/interfaces/Settings/Logs'
|
||||
import LogsPreviewer from 'components/interfaces/Settings/Logs/LogsPreviewer'
|
||||
|
||||
export const LogPage: NextPage = () => {
|
||||
const router = useRouter()
|
||||
const { ref, source } = router.query
|
||||
|
||||
// quick fix to get correct queryType
|
||||
const queryType = {
|
||||
edge_logs: 'api',
|
||||
postgres_logs: 'database',
|
||||
function_logs: 'functions',
|
||||
function_edge_logs: 'fn_edge',
|
||||
}
|
||||
|
||||
return (
|
||||
<LogsExplorerLayout subtitle={<span className="font-mono">{source}</span>}>
|
||||
<LogsPreviewer
|
||||
projectRef={ref as string}
|
||||
condensedLayout={false}
|
||||
tableName={source as LogsTableName}
|
||||
// @ts-ignore
|
||||
queryType={queryType[source]}
|
||||
/>
|
||||
</LogsExplorerLayout>
|
||||
)
|
||||
}
|
||||
|
||||
export default withAuth(observer(LogPage))
|
||||
@@ -52,7 +52,7 @@ const coolGray = {
|
||||
|
||||
module.exports = ui({
|
||||
darkMode: 'class', // or 'media' or 'class'
|
||||
purge: [
|
||||
content: [
|
||||
// purge styles from app
|
||||
'./pages/**/*.{js,ts,jsx,tsx}',
|
||||
'./components/**/*.{js,ts,jsx,tsx}',
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
import { genDefaultQuery, LogsTableName } from 'components/interfaces/Settings/Logs'
|
||||
|
||||
describe.each(Object.values(LogsTableName))('%s', (table) => {
|
||||
describe.each([
|
||||
{ filter: { search_query: '' } },
|
||||
{ filter: { search_query: undefined } },
|
||||
{ filter: {} },
|
||||
{ filter: { search_query: '123test' }, contains: ['123test'] },
|
||||
// test behaviour with overrides
|
||||
{
|
||||
filter: { search_query: '123test', override: 'something' },
|
||||
contains: ['123test', 'override', 'something'],
|
||||
},
|
||||
{
|
||||
filter: { search_query: '', override: 'something' },
|
||||
contains: ['override', 'something'],
|
||||
},
|
||||
{
|
||||
filter: { search_query: undefined, override: 'something' },
|
||||
contains: ['override', 'something'],
|
||||
},
|
||||
{
|
||||
filter: { override: 'something' },
|
||||
contains: ['override', 'something'],
|
||||
},
|
||||
{
|
||||
filter: { 'override.nested': 'something' },
|
||||
contains: ['override.nested', 'something'],
|
||||
},
|
||||
])('generates sql for filter $filter', ({ filter, contains = [] }) => {
|
||||
test('generates query correctly', async () => {
|
||||
const generated = genDefaultQuery(table, filter)
|
||||
// should not contain functions
|
||||
expect(generated).not.toMatch(/function[ A-Za-z_-]+\(.+\).+\{.+return.+\}/)
|
||||
expect(generated).not.toMatch(/\=\>|\$\{.+\}/)
|
||||
expect(generated).not.toContain('return')
|
||||
|
||||
contains.forEach((str) => expect(generated).toContain(str))
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -291,4 +291,33 @@ test("bug: nav to explorer preserves newlines", async ()=>{
|
||||
const router = useRouter()
|
||||
userEvent.click(await screen.findByText(/Explore/))
|
||||
await expect(router.push).toBeCalledWith(expect.stringContaining(encodeURIComponent("\n")))
|
||||
}
|
||||
}
|
||||
test('filters alter generated query', async () => {
|
||||
render(<LogsPreviewer projectRef="123" tableName={LogsTableName.EDGE} />)
|
||||
userEvent.click(await screen.findByRole('button', { name: 'Status' }))
|
||||
userEvent.click(await screen.findByText(/500 error codes/))
|
||||
userEvent.click(await screen.findByText(/200 codes/))
|
||||
userEvent.click(await screen.findByText(/Save/))
|
||||
|
||||
await waitFor(() => {
|
||||
expect(get).toHaveBeenCalledWith(expect.stringContaining('select'))
|
||||
expect(get).toHaveBeenCalledWith(expect.stringContaining('500'))
|
||||
expect(get).toHaveBeenCalledWith(expect.stringContaining('200'))
|
||||
expect(get).toHaveBeenCalledWith(expect.stringContaining('where'))
|
||||
expect(get).toHaveBeenCalledWith(expect.stringContaining('and'))
|
||||
})
|
||||
})
|
||||
test('filters accept filterOverride', async () => {
|
||||
render(
|
||||
<LogsPreviewer
|
||||
projectRef="123"
|
||||
tableName={LogsTableName.FUNCTIONS}
|
||||
filterOverride={{ 'my.nestedkey': 'myvalue' }}
|
||||
/>
|
||||
)
|
||||
|
||||
await waitFor(() => {
|
||||
expect(get).toHaveBeenCalledWith(expect.stringContaining('my.nestedkey'))
|
||||
expect(get).toHaveBeenCalledWith(expect.stringContaining('myvalue'))
|
||||
})
|
||||
})
|
||||
@@ -1,287 +0,0 @@
|
||||
// mock the fetch function
|
||||
jest.mock('lib/common/fetch')
|
||||
import { get } from 'lib/common/fetch'
|
||||
|
||||
// mock mobx
|
||||
jest.mock('mobx-react-lite')
|
||||
import { observer } from 'mobx-react-lite'
|
||||
observer.mockImplementation((v) => v)
|
||||
|
||||
// mock the router
|
||||
jest.mock('next/router')
|
||||
import { useRouter } from 'next/router'
|
||||
const defaultRouterMock = () => {
|
||||
const router = jest.fn()
|
||||
router.query = {}
|
||||
router.push = jest.fn()
|
||||
router.pathname = 'logs/path'
|
||||
return router
|
||||
}
|
||||
useRouter.mockReturnValue(defaultRouterMock())
|
||||
|
||||
// mock monaco editor
|
||||
jest.mock('@monaco-editor/react')
|
||||
import Editor, { useMonaco } from '@monaco-editor/react'
|
||||
Editor = jest.fn()
|
||||
Editor.mockImplementation((props) => {
|
||||
return (
|
||||
<textarea className="monaco-editor" onChange={(e) => props.onChange(e.target.value)}></textarea>
|
||||
)
|
||||
})
|
||||
useMonaco.mockImplementation((v) => v)
|
||||
|
||||
// mock usage flags
|
||||
jest.mock('components/ui/Flag/Flag')
|
||||
import Flag from 'components/ui/Flag/Flag'
|
||||
Flag.mockImplementation(({ children }) => <>{children}</>)
|
||||
jest.mock('hooks')
|
||||
import { useFlag } from 'hooks'
|
||||
useFlag.mockReturnValue(true)
|
||||
|
||||
import { SWRConfig } from 'swr'
|
||||
jest.mock('components/interfaces/Settings/Logs/LogsPreviewer')
|
||||
import LogsPreviewer from 'components/interfaces/Settings/Logs/LogsPreviewer'
|
||||
LogsPreviewer.mockImplementation((props) => {
|
||||
const Comp = jest.requireActual('components/interfaces/Settings/Logs/LogsPreviewer').default
|
||||
// wrap with SWR to reset the cache each time
|
||||
return (
|
||||
<SWRConfig
|
||||
value={{
|
||||
provider: () => new Map(),
|
||||
shouldRetryOnError: false,
|
||||
}}
|
||||
>
|
||||
<Comp {...props} />
|
||||
</SWRConfig>
|
||||
)
|
||||
})
|
||||
|
||||
import { render, fireEvent, waitFor, screen, act } from '@testing-library/react'
|
||||
import userEvent from '@testing-library/user-event'
|
||||
import { getToggleByText } from '../../helpers'
|
||||
import { wait } from '@testing-library/user-event/dist/utils'
|
||||
import { logDataFixture } from '../../fixtures'
|
||||
import { LogsTableName } from 'components/interfaces/Settings/Logs'
|
||||
import { find } from 'lodash'
|
||||
beforeEach(() => {
|
||||
// reset mocks between tests
|
||||
get.mockReset()
|
||||
useRouter.mockReset()
|
||||
useRouter.mockReturnValue(defaultRouterMock())
|
||||
})
|
||||
test('can display log data and metadata', async () => {
|
||||
get.mockResolvedValue({
|
||||
result: [
|
||||
logDataFixture({
|
||||
id: 'some-event-happened-id',
|
||||
metadata: {
|
||||
my_key: 'something_value',
|
||||
},
|
||||
}),
|
||||
],
|
||||
})
|
||||
render(<LogsPreviewer projectRef="123" tableName={LogsTableName.EDGE} />)
|
||||
fireEvent.click(await screen.findByText(/some-event-happened-id/))
|
||||
await screen.findByText(/my_key/)
|
||||
await screen.findByText(/something_value/)
|
||||
})
|
||||
|
||||
test('Refresh page', async () => {
|
||||
get.mockImplementation((url) => {
|
||||
if (url.includes('count')) return { result: { count: 0 } }
|
||||
return {
|
||||
result: [
|
||||
logDataFixture({
|
||||
id: 'some-event-id',
|
||||
metadata: { my_key: 'something_value' },
|
||||
}),
|
||||
],
|
||||
}
|
||||
})
|
||||
render(<LogsPreviewer projectRef="123" tableName={LogsTableName.EDGE} />)
|
||||
|
||||
const row = await screen.findByText(/some-event-id/)
|
||||
fireEvent.click(row)
|
||||
await screen.findByText(/my_key/)
|
||||
})
|
||||
|
||||
test('Search will trigger a log refresh', async () => {
|
||||
get.mockImplementation((url) => {
|
||||
if (url.includes('something')) {
|
||||
return {
|
||||
result: [logDataFixture({ id: 'some-event-id' })],
|
||||
}
|
||||
}
|
||||
return { result: [] }
|
||||
})
|
||||
render(<LogsPreviewer projectRef="123" tableName={LogsTableName.EDGE} />)
|
||||
|
||||
userEvent.type(screen.getByPlaceholderText(/Search events/), 'something{enter}')
|
||||
|
||||
await waitFor(
|
||||
() => {
|
||||
expect(get).toHaveBeenCalledWith(expect.stringContaining('something'))
|
||||
|
||||
// updates router query params
|
||||
const router = useRouter()
|
||||
expect(router.push).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
pathname: expect.any(String),
|
||||
query: expect.objectContaining({
|
||||
s: expect.stringContaining('something'),
|
||||
}),
|
||||
})
|
||||
)
|
||||
},
|
||||
{ timeout: 1500 }
|
||||
)
|
||||
await screen.findByText(/some-event-id/)
|
||||
})
|
||||
|
||||
test('poll count for new messages', async () => {
|
||||
get.mockImplementation((url) => {
|
||||
if (url.includes('count')) {
|
||||
return { result: [{ count: 125 }] }
|
||||
}
|
||||
return {
|
||||
result: [logDataFixture({ id: 'some-uuid123' })],
|
||||
}
|
||||
})
|
||||
render(<LogsPreviewer projectRef="123" tableName={LogsTableName.EDGE} />)
|
||||
await waitFor(() => screen.queryByText(/some-uuid123/) === null)
|
||||
// should display new logs count
|
||||
await waitFor(() => screen.getByText(/125/))
|
||||
|
||||
userEvent.click(screen.getByText(/Refresh/))
|
||||
await waitFor(() => screen.queryByText(/125/) === null)
|
||||
await screen.findByText(/some-uuid123/)
|
||||
})
|
||||
|
||||
test('s= query param will populate the search bar', async () => {
|
||||
const router = defaultRouterMock()
|
||||
router.query = { ...router.query, s: 'someSearch' }
|
||||
useRouter.mockReturnValue(router)
|
||||
render(<LogsPreviewer projectRef="123" tableName={LogsTableName.EDGE} />)
|
||||
// should populate search input with the search param
|
||||
await screen.findByDisplayValue('someSearch')
|
||||
expect(get).toHaveBeenCalledWith(expect.stringContaining('someSearch'))
|
||||
})
|
||||
|
||||
test('te= query param will populate the timestamp to input', async () => {
|
||||
// get time 20 mins before
|
||||
const newDate = new Date()
|
||||
newDate.setMinutes(new Date().getMinutes() - 20)
|
||||
const isoString = newDate.toISOString()
|
||||
const unixMicro = newDate.getTime() * 1000 //microseconds
|
||||
const router = defaultRouterMock()
|
||||
router.query = { ...router.query, te: unixMicro }
|
||||
useRouter.mockReturnValue(router)
|
||||
render(<LogsPreviewer projectRef="123" tableName={LogsTableName.EDGE} />)
|
||||
|
||||
await waitFor(() => {
|
||||
expect(get).toHaveBeenCalledWith(
|
||||
expect.stringContaining(`timestamp_end=${encodeURIComponent(unixMicro)}`)
|
||||
)
|
||||
})
|
||||
userEvent.click(await screen.findByText('Custom'))
|
||||
expect(get).toHaveBeenCalledWith(expect.stringContaining('timestamp_end=' + unixMicro))
|
||||
})
|
||||
test('ts= query param will populate the timestamp from input', async () => {
|
||||
// get time 20 mins before
|
||||
const newDate = new Date()
|
||||
newDate.setMinutes(new Date().getMinutes() - 20)
|
||||
const isoString = newDate.toISOString()
|
||||
const unixMicro = newDate.getTime() * 1000 //microseconds
|
||||
const router = defaultRouterMock()
|
||||
router.query = { ...router.query, ts: unixMicro }
|
||||
useRouter.mockReturnValue(router)
|
||||
render(<LogsPreviewer projectRef="123" tableName={LogsTableName.EDGE} />)
|
||||
|
||||
await waitFor(() => {
|
||||
expect(get).toHaveBeenCalledWith(
|
||||
expect.stringContaining(`timestamp_start=${encodeURIComponent(unixMicro)}`)
|
||||
)
|
||||
})
|
||||
userEvent.click(await screen.findByText('Custom'))
|
||||
await screen.findByText(new RegExp(newDate.getFullYear()))
|
||||
await screen.findByText(/Apply/)
|
||||
expect(get).toHaveBeenCalledWith(expect.stringContaining('timestamp_start=' + unixMicro))
|
||||
})
|
||||
|
||||
test('load older btn will fetch older logs', async () => {
|
||||
get.mockImplementation((url) => {
|
||||
if (url.includes('count')) {
|
||||
return {}
|
||||
}
|
||||
return {
|
||||
result: [logDataFixture({ id: 'first event' })],
|
||||
}
|
||||
})
|
||||
render(<LogsPreviewer projectRef="123" tableName={LogsTableName.EDGE} />)
|
||||
// should display first log but not second
|
||||
await waitFor(() => screen.getByText('first event'))
|
||||
await expect(screen.findByText('second event')).rejects.toThrow()
|
||||
|
||||
get.mockResolvedValueOnce({
|
||||
result: [logDataFixture({ id: 'second event' })],
|
||||
})
|
||||
// should display first and second log
|
||||
userEvent.click(await screen.findByText('Load older'))
|
||||
await screen.findByText('first event')
|
||||
await screen.findByText('second event')
|
||||
expect(get).toHaveBeenCalledWith(expect.stringContaining('timestamp_end='))
|
||||
})
|
||||
|
||||
test('bug: load older btn does not error out when previous page is empty', async () => {
|
||||
// bugfix for https://sentry.io/organizations/supabase/issues/2903331460/?project=5459134&referrer=slack
|
||||
get.mockImplementation((url) => {
|
||||
if (url.includes('count')) {
|
||||
return {}
|
||||
}
|
||||
return { result: [] }
|
||||
})
|
||||
render(<LogsPreviewer projectRef="123" tableName={LogsTableName.EDGE} />)
|
||||
|
||||
userEvent.click(await screen.findByText('Load older'))
|
||||
// NOTE: potential race condition, since we are asserting that something DOES NOT EXIST
|
||||
// wait for 500s to make sure all ui logic is complete
|
||||
// need to wrap in act because internal react state is changing during this time.
|
||||
await act(async () => await wait(100))
|
||||
|
||||
// clicking load older multiple times should not give error
|
||||
await waitFor(() => {
|
||||
expect(screen.queryByText(/Sorry/)).toBeNull()
|
||||
expect(screen.queryByText(/An error occured/)).toBeNull()
|
||||
expect(screen.queryByText(/undefined/)).toBeNull()
|
||||
})
|
||||
})
|
||||
|
||||
test('log event chart hide', async () => {
|
||||
render(<LogsPreviewer projectRef="123" tableName={LogsTableName.EDGE} />)
|
||||
await screen.findByText('Events')
|
||||
const toggle = await screen.findByText(/Chart/)
|
||||
userEvent.click(toggle)
|
||||
await expect(screen.findByText('Events')).rejects.toThrow()
|
||||
})
|
||||
|
||||
test('bug: nav backwards with params change results in ui changing', async () => {
|
||||
// bugfix for https://sentry.io/organizations/supabase/issues/2903331460/?project=5459134&referrer=slack
|
||||
get.mockImplementation((url) => {
|
||||
if (url.includes('count')) {
|
||||
return {}
|
||||
}
|
||||
return { data: [] }
|
||||
})
|
||||
const { container, rerender } = render(
|
||||
<LogsPreviewer projectRef="123" tableName={LogsTableName.EDGE} />
|
||||
)
|
||||
|
||||
await expect(screen.findByDisplayValue('simple-query')).rejects.toThrow()
|
||||
|
||||
const router = defaultRouterMock()
|
||||
router.query = { ...router.query, s: 'simple-query' }
|
||||
useRouter.mockReturnValue(router)
|
||||
rerender(<LogsPreviewer projectRef="123" tableName={LogsTableName.EDGE} />)
|
||||
|
||||
await screen.findByDisplayValue('simple-query')
|
||||
})
|
||||
@@ -91,7 +91,6 @@ test('can display log data', async () => {
|
||||
|
||||
userEvent.click(await screen.findByText(/Run/))
|
||||
const row = await screen.findByText("some-event-happened")
|
||||
screen.debug()
|
||||
userEvent.click(row)
|
||||
await screen.findByText(/something_value/)
|
||||
})
|
||||
|
||||
+8
-17
@@ -54,11 +54,6 @@ GraphQL is in Beta, and may have breaking changes. It is only available on self-
|
||||
:::
|
||||
|
||||
GraphQL in Supabase works through [pg_graphql](/blog/2021/12/03/pg-graphql), an open source PostgreSQL extension for GraphQL.
|
||||
To get started with GraphQL you must enable the `pg_graphql` extension in your project.
|
||||
|
||||
<video width="99%" muted playsInline controls="true">
|
||||
<source src="/docs/videos/api/api-enable-graphql.mp4" type="video/mp4" muted playsInline />
|
||||
</video>
|
||||
|
||||
### Realtime API
|
||||
|
||||
@@ -240,7 +235,8 @@ JS Reference: [select()](/docs/reference/javascript/select),
|
||||
|
||||
:::note
|
||||
|
||||
While pg_graphql is in beta, we have disabled GraphQL auto-refreshing. If you want to refresh your GraphQL introspection, call `select graphql.rebuild_schema();` to rebuild your GraphQL schema.
|
||||
To rebuild your GraphQL schema from the SQL schema, call `select graphql.rebuild_schema();`.
|
||||
Be sure to rebuild the GraphQL schema after altering the SQL schema.
|
||||
|
||||
:::
|
||||
|
||||
@@ -426,19 +422,14 @@ If they detect any keys with service_role privileges being pushed to GitHub, the
|
||||
|
||||
### Safeguards towards accidental deletes and updates
|
||||
|
||||
For new projects, by default, the Postgres extension [safeupdate](https://github.com/eradman/pg-safeupdate) is enabled for all queries coming from the API.
|
||||
For all projects, by default, the Postgres extension [safeupdate](https://github.com/eradman/pg-safeupdate) is enabled for all queries coming from the API.
|
||||
This ensures that any `delete()` or `update()` would fail if there are no accompanying filters provided.
|
||||
This can be disabled by running the following query on the SQL editor on the dashboard:
|
||||
|
||||
To confirm that safeupdate is enabled for queries going through the API of your project, the following query could be run:
|
||||
```sql
|
||||
alter role "authenticator"
|
||||
set "session_preload_libraries" to default;
|
||||
select usename,useconfig from pg_shadow where usename = 'authenticator' ;
|
||||
```
|
||||
|
||||
To reenable it :
|
||||
```sql
|
||||
alter role "authenticator"
|
||||
set "session_preload_libraries" = 'safeupdate';
|
||||
The expected value for `useconfig` should be:
|
||||
```
|
||||
The above can also be run to enable it for existing projects. Settings are applied immediately and as such no restarts are needed after running any of the two.
|
||||
|
||||
["session_preload_libraries=supautils, safeupdate"]
|
||||
```
|
||||
@@ -27,7 +27,7 @@ There are two parts to every Auth system:
|
||||
- **Authorization:** once they are in, what are they allowed to do?
|
||||
|
||||
Supabase Auth is designed to work either as a standalone product, or deeply integrated with the other Supabase products.
|
||||
Postgres is at the heart of everything we do, and the Auth system follows this principle. We leverage Postgres' build-in Auth functionality wherever possible.
|
||||
Postgres is at the heart of everything we do, and the Auth system follows this principle. We leverage Postgres' built-in Auth functionality wherever possible.
|
||||
|
||||
## Authentication
|
||||
|
||||
|
||||
@@ -87,9 +87,9 @@ async function signInWithEmail() {
|
||||
<TabItem value="dart">
|
||||
|
||||
```dart
|
||||
function SignInWithEmail() {
|
||||
Future<void> signInWithEmail() async {
|
||||
await supabase.auth.signIn(
|
||||
email: 'example@email.com',
|
||||
email: 'example@email.com',
|
||||
password: 'example-password'
|
||||
);
|
||||
}
|
||||
@@ -120,8 +120,8 @@ async function signOut() {
|
||||
<TabItem value="dart">
|
||||
|
||||
```dart
|
||||
function SignOut() {
|
||||
await supabase.auth.signOut();
|
||||
Future<void> signOut() async {
|
||||
await supabase.auth.signOut();
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
@@ -24,7 +24,7 @@ Setting up LinkedIn logins for your application consists of 3 parts:
|
||||
- Go to [LinkedIn Developer Dashboard](https://www.linkedin.com/developers/apps).
|
||||
- Log in (if necessary.)
|
||||
|
||||

|
||||

|
||||
|
||||
### Find your callback URL
|
||||
|
||||
|
||||
@@ -83,10 +83,8 @@ async function signInWithEmail() {
|
||||
<TabItem value="dart">
|
||||
|
||||
```dart
|
||||
function SignInWithEmail() {
|
||||
await supabase.auth.signIn(
|
||||
email: 'example@email.com'
|
||||
);
|
||||
Future<void> signInWithEmail() async {
|
||||
await supabase.auth.signIn(email: 'example@email.com');
|
||||
}
|
||||
```
|
||||
|
||||
@@ -115,7 +113,7 @@ async function signOut() {
|
||||
<TabItem value="dart">
|
||||
|
||||
```dart
|
||||
function SignOut() {
|
||||
Future<void> signOut() async {
|
||||
await supabase.auth.signOut();
|
||||
}
|
||||
```
|
||||
|
||||
@@ -107,7 +107,7 @@ rollback;
|
||||
|
||||
API:
|
||||
|
||||
- [`hasTable()`](https://pgtap.org/documentation.html#has_table)
|
||||
- [`has_table()`](https://pgtap.org/documentation.html#has_table)
|
||||
|
||||
|
||||
### Testing columns
|
||||
@@ -130,4 +130,4 @@ API:
|
||||
|
||||
## Resources
|
||||
|
||||
- Official [`pgTAP` documentation](https://pgtap.org/).
|
||||
- Official [`pgTAP` documentation](https://pgtap.org/).
|
||||
@@ -17,7 +17,7 @@ pgMustard requires plans to be in json format, and the buffers, verbose, and set
|
||||
So a good prefix for your query would be:
|
||||
|
||||
```jsx
|
||||
explain (analyze, format json, buffers, verbose, settings)`
|
||||
explain (analyze, format json, buffers, verbose, settings)
|
||||
```
|
||||
|
||||
Run the query, and copy the output.
|
||||
|
||||
@@ -419,6 +419,9 @@ And then open the browser to [localhost:3000](http://localhost:3000) and you sho
|
||||
|
||||
Every Supabase project is configured with [Storage](/docs/guides/storage) for managing large files like photos and videos.
|
||||
|
||||
### Add @reach/visually-hidden
|
||||
The upload widget uses one additional npm library. You can install it like so: `npm install @reach/visually-hidden`.
|
||||
|
||||
### Create an upload widget
|
||||
|
||||
Let's create an avatar for the user so that they can upload a profile photo. We can start by creating a new component:
|
||||
|
||||
File renamed without changes.
Binary file not shown.
+1
-1
@@ -1001,7 +1001,7 @@ const Beta = (props: Props) => {
|
||||
<div className="flex items-center justify-between px-5 py-5 shadow-lg xl:px-20 bg-scale-1200 dark:bg-scale-300">
|
||||
<Link href="/">
|
||||
<a>
|
||||
<Image src={`${basePath}/images/logo-dark.png`} height={20} width={20} />
|
||||
<Image src={`${basePath}/images/logo-dark.png`} height={24} width={120} />
|
||||
</a>
|
||||
</Link>
|
||||
<HamburgerMenu openMenu={() => setMenuOpen(!menuOpen)} />
|
||||
|
||||
@@ -3,7 +3,7 @@ const svgToDataUri = require('mini-svg-data-uri')
|
||||
|
||||
module.exports = ui({
|
||||
mode: 'JIT',
|
||||
purge: [
|
||||
content: [
|
||||
'./components/**/*.tsx',
|
||||
'./pages/**/*.tsx',
|
||||
'./_blog/*.mdx',
|
||||
|
||||
Reference in new issue
Block a user