diff --git a/apps/studio/components/interfaces/Auth/Policies/PolicyEditor/PolicyRoles.tsx b/apps/studio/components/interfaces/Auth/Policies/PolicyEditor/PolicyRoles.tsx index c8373a59039..4586d246c21 100644 --- a/apps/studio/components/interfaces/Auth/Policies/PolicyEditor/PolicyRoles.tsx +++ b/apps/studio/components/interfaces/Auth/Policies/PolicyEditor/PolicyRoles.tsx @@ -43,7 +43,7 @@ const PolicyRoles = ({ selectedRoles, onUpdateSelectedRoles }: PolicyRolesProps)
{isLoading && } - {isError && } + {isError && } {isSuccess && ( { @@ -54,8 +54,8 @@ const CreateRolePanel = ({ visible, onClose }: CreateRolePanelProps) => { }) const { mutate: createDatabaseRole, isLoading: isCreating } = useDatabaseRoleCreateMutation({ - onSuccess: (res) => { - toast.success(`Successfully created new role: ${res.name}`) + onSuccess: (_, vars) => { + toast.success(`Successfully created new role: ${vars.payload.name}`) handleClose() }, }) diff --git a/apps/studio/components/interfaces/Database/Roles/DeleteRoleModal.tsx b/apps/studio/components/interfaces/Database/Roles/DeleteRoleModal.tsx index efb5dfdd93c..4422024b5c2 100644 --- a/apps/studio/components/interfaces/Database/Roles/DeleteRoleModal.tsx +++ b/apps/studio/components/interfaces/Database/Roles/DeleteRoleModal.tsx @@ -27,7 +27,7 @@ const DeleteRoleModal = ({ role, visible, onClose }: DeleteRoleModalProps) => { deleteDatabaseRole({ projectRef: project.ref, connectionString: project.connectionString, - id: role.id.toString(), + id: role.id, }) } diff --git a/apps/studio/components/interfaces/Database/Roles/RoleRow.tsx b/apps/studio/components/interfaces/Database/Roles/RoleRow.tsx index a138fd08ee9..ae1f6eb55bd 100644 --- a/apps/studio/components/interfaces/Database/Roles/RoleRow.tsx +++ b/apps/studio/components/interfaces/Database/Roles/RoleRow.tsx @@ -1,5 +1,4 @@ import * as Tooltip from '@radix-ui/react-tooltip' -import type { PostgresRole } from '@supabase/postgres-meta' import { useState } from 'react' import toast from 'react-hot-toast' import { @@ -18,13 +17,14 @@ import { } from 'ui' import { useProjectContext } from 'components/layouts/ProjectLayout/ProjectContext' +import { PgRole } from 'data/database-roles/database-roles-query' import { useDatabaseRoleUpdateMutation } from 'data/database-roles/database-role-update-mutation' import { ROLE_PERMISSIONS } from './Roles.constants' interface RoleRowProps { - role: PostgresRole + role: PgRole disabled?: boolean - onSelectDelete: (role: PostgresRole) => void + onSelectDelete: (role: PgRole) => void } const RoleRow = ({ role, disabled = false, onSelectDelete }: RoleRowProps) => { @@ -33,25 +33,22 @@ const RoleRow = ({ role, disabled = false, onSelectDelete }: RoleRowProps) => { const { mutate: updateDatabaseRole, isLoading: isUpdating } = useDatabaseRoleUpdateMutation() - const { - is_superuser, - can_login, - can_create_role, - can_create_db, - is_replication_role, - can_bypass_rls, - } = role + const { isSuperuser, canLogin, canCreateRole, canCreateDb, isReplicationRole, canBypassRls } = + role - const onSaveChanges = async (values: any, { resetForm }: any) => { + const onSaveChanges = async (values: Partial, { resetForm }: any) => { if (!project) return console.error('Project is required') - const { is_superuser, is_replication_role, ...payload } = values + const changed = Object.fromEntries( + Object.entries(values).filter(([k, v]) => v !== (role as any)[k]) + ) + updateDatabaseRole( { projectRef: project.ref, connectionString: project.connectionString, id: role.id, - payload, + payload: changed, }, { onSuccess: () => { @@ -66,12 +63,12 @@ const RoleRow = ({ role, disabled = false, onSelectDelete }: RoleRowProps) => {
{
- {role.active_connections > 0 && ( + {role.activeConnections > 0 && (
@@ -129,10 +126,10 @@ const RoleRow = ({ role, disabled = false, onSelectDelete }: RoleRowProps) => {

0 ? 'text-foreground' : 'text-foreground-light' + role.activeConnections > 0 ? 'text-foreground' : 'text-foreground-light' }`} > - {role.active_connections} connections + {role.activeConnections} connections

{!disabled && ( diff --git a/apps/studio/components/interfaces/Database/Roles/Roles.constants.ts b/apps/studio/components/interfaces/Database/Roles/Roles.constants.ts index 6713ec7e150..377dd9589c6 100644 --- a/apps/studio/components/interfaces/Database/Roles/Roles.constants.ts +++ b/apps/studio/components/interfaces/Database/Roles/Roles.constants.ts @@ -33,32 +33,32 @@ export const SYSTEM_ROLES = [ ] as const export const ROLE_PERMISSIONS = { - can_login: { + canLogin: { disabled: false, description: 'User can login', grant_by_dashboard: true, }, - can_create_role: { + canCreateRole: { disabled: false, description: 'User can create roles', grant_by_dashboard: true, }, - can_create_db: { + canCreateDb: { disabled: false, description: 'User can create databases', grant_by_dashboard: true, }, - can_bypass_rls: { + canBypassRls: { disabled: false, description: 'User bypasses every row level security policy', grant_by_dashboard: true, }, - is_superuser: { + isSuperuser: { disabled: true, description: 'User is a Superuser', grant_by_dashboard: false, }, - is_replication_role: { + isReplicationRole: { disabled: false, description: 'User can initiate streaming replication and put the system in and out of backup mode', diff --git a/apps/studio/components/interfaces/Database/Roles/RolesList.tsx b/apps/studio/components/interfaces/Database/Roles/RolesList.tsx index 302328cac7c..089efbe930e 100644 --- a/apps/studio/components/interfaces/Database/Roles/RolesList.tsx +++ b/apps/studio/components/interfaces/Database/Roles/RolesList.tsx @@ -42,19 +42,19 @@ const RolesList = () => { const roles = sortBy(data ?? [], (r) => r.name.toLocaleLowerCase()) const filteredRoles = ( - filterType === 'active' ? roles.filter((role) => role.active_connections > 0) : roles + filterType === 'active' ? roles.filter((role) => role.activeConnections > 0) : roles ).filter((role) => role.name.includes(filterString)) const [supabaseRoles, otherRoles] = partition(filteredRoles, (role) => SUPABASE_ROLES.includes(role.name as SUPABASE_ROLE) ) const totalActiveConnections = roles - .map((role) => role.active_connections) + .map((role) => role.activeConnections) .reduce((a, b) => a + b, 0) // order the roles with active connections by number of connections, most connections first const rolesWithActiveConnections = sortBy( - roles.filter((role) => role.active_connections > 0), - (r) => -r.active_connections + roles.filter((role) => role.activeConnections > 0), + (r) => -r.activeConnections ) return ( @@ -145,7 +145,7 @@ const RolesList = () => {

Connections by roles:

{rolesWithActiveConnections.map((role) => (
- {role.name}: {role.active_connections} + {role.name}: {role.activeConnections}
))}
diff --git a/apps/studio/data/database-roles/database-role-create-mutation.ts b/apps/studio/data/database-roles/database-role-create-mutation.ts index ecaaa44fa47..7601b21e30d 100644 --- a/apps/studio/data/database-roles/database-role-create-mutation.ts +++ b/apps/studio/data/database-roles/database-role-create-mutation.ts @@ -1,12 +1,12 @@ import { useMutation, UseMutationOptions, useQueryClient } from '@tanstack/react-query' +import pgMeta from '@supabase/pg-meta' import { toast } from 'react-hot-toast' -import { post } from 'data/fetchers' import type { ResponseError } from 'types' -import { databaseRolesKeys } from './keys' -import type { components } from 'data/api' +import { executeSql } from 'data/sql/execute-sql-query' +import { invalidateRolesQuery } from './database-roles-query' -type CreateRoleBody = components['schemas']['CreateRoleBody'] +type CreateRoleBody = Parameters[0] export type DatabaseRoleCreateVariables = { projectRef: string @@ -19,20 +19,14 @@ export async function createDatabaseRole({ connectionString, payload, }: DatabaseRoleCreateVariables) { - let headers = new Headers() - if (connectionString) headers.set('x-connection-encrypted', connectionString) - - const { data, error } = await post('/platform/pg-meta/{ref}/roles', { - params: { - header: { 'x-connection-encrypted': connectionString! }, - path: { ref: projectRef }, - }, - body: payload, - headers, + const sql = pgMeta.roles.create(payload).sql + const { result } = await executeSql({ + projectRef, + connectionString, + sql, + queryKey: ['roles', 'create'], }) - - if (error) throw error - return data + return result } type DatabaseRoleCreateData = Awaited> @@ -52,7 +46,7 @@ export const useDatabaseRoleCreateMutation = ({ { async onSuccess(data, variables, context) { const { projectRef } = variables - await queryClient.invalidateQueries(databaseRolesKeys.list(projectRef)) + await invalidateRolesQuery(queryClient, projectRef) await onSuccess?.(data, variables, context) }, async onError(data, variables, context) { diff --git a/apps/studio/data/database-roles/database-role-delete-mutation.ts b/apps/studio/data/database-roles/database-role-delete-mutation.ts index ac0569614ab..c2e7ddda4e7 100644 --- a/apps/studio/data/database-roles/database-role-delete-mutation.ts +++ b/apps/studio/data/database-roles/database-role-delete-mutation.ts @@ -1,35 +1,34 @@ import { useMutation, UseMutationOptions, useQueryClient } from '@tanstack/react-query' +import pgMeta from '@supabase/pg-meta' import { toast } from 'react-hot-toast' -import { del } from 'data/fetchers' import type { ResponseError } from 'types' -import { databaseRolesKeys } from './keys' +import { executeSql } from 'data/sql/execute-sql-query' +import { invalidateRolesQuery } from './database-roles-query' + +type DropRoleBody = Parameters[1] export type DatabaseRoleDeleteVariables = { projectRef: string connectionString?: string - id: string + id: number + payload?: DropRoleBody } export async function deleteDatabaseRole({ projectRef, connectionString, id, + payload, }: DatabaseRoleDeleteVariables) { - let headers = new Headers() - if (connectionString) headers.set('x-connection-encrypted', connectionString) - - const { data, error } = await del('/platform/pg-meta/{ref}/roles', { - params: { - header: { 'x-connection-encrypted': connectionString! }, - path: { ref: projectRef }, - query: { id }, - }, - headers, + const sql = pgMeta.roles.remove({ id }, payload).sql + const { result } = await executeSql({ + projectRef, + connectionString, + sql, + queryKey: ['roles', 'delete'], }) - - if (error) throw error - return data + return result } type DatabaseRoleDeleteData = Awaited> @@ -49,7 +48,7 @@ export const useDatabaseRoleDeleteMutation = ({ { async onSuccess(data, variables, context) { const { projectRef } = variables - await queryClient.invalidateQueries(databaseRolesKeys.list(projectRef)) + await invalidateRolesQuery(queryClient, projectRef) await onSuccess?.(data, variables, context) }, async onError(data, variables, context) { diff --git a/apps/studio/data/database-roles/database-role-update-mutation.ts b/apps/studio/data/database-roles/database-role-update-mutation.ts index c8876fb55e7..a0d90520269 100644 --- a/apps/studio/data/database-roles/database-role-update-mutation.ts +++ b/apps/studio/data/database-roles/database-role-update-mutation.ts @@ -1,12 +1,12 @@ import { useMutation, UseMutationOptions, useQueryClient } from '@tanstack/react-query' +import pgMeta from '@supabase/pg-meta' import { toast } from 'react-hot-toast' -import { patch } from 'data/fetchers' import type { ResponseError } from 'types' -import { databaseRolesKeys } from './keys' -import type { components } from 'data/api' +import { executeSql } from 'data/sql/execute-sql-query' +import { invalidateRolesQuery } from './database-roles-query' -type UpdateRoleBody = components['schemas']['UpdateRoleBody'] +type UpdateRoleBody = Parameters[1] export type DatabaseRoleUpdateVariables = { projectRef: string @@ -21,21 +21,14 @@ export async function updateDatabaseRole({ id, payload, }: DatabaseRoleUpdateVariables) { - let headers = new Headers() - if (connectionString) headers.set('x-connection-encrypted', connectionString) - - const { data, error } = await patch('/platform/pg-meta/{ref}/roles', { - params: { - header: { 'x-connection-encrypted': connectionString! }, - path: { ref: projectRef }, - query: { id }, - }, - body: payload, - headers, + const sql = pgMeta.roles.update({ id }, payload).sql + const { result } = await executeSql({ + projectRef, + connectionString, + sql, + queryKey: ['roles', 'update'], }) - - if (error) throw error - return data + return result } type DatabaseRoleUpdateData = Awaited> @@ -55,7 +48,7 @@ export const useDatabaseRoleUpdateMutation = ({ { async onSuccess(data, variables, context) { const { projectRef } = variables - await queryClient.invalidateQueries(databaseRolesKeys.list(projectRef)) + await invalidateRolesQuery(queryClient, projectRef) await onSuccess?.(data, variables, context) }, async onError(data, variables, context) { diff --git a/apps/studio/data/database-roles/database-roles-query.ts b/apps/studio/data/database-roles/database-roles-query.ts index fc107b656a2..e347af6f236 100644 --- a/apps/studio/data/database-roles/database-roles-query.ts +++ b/apps/studio/data/database-roles/database-roles-query.ts @@ -1,49 +1,41 @@ -import { UseQueryOptions, useQuery } from '@tanstack/react-query' -import type { PostgresRole } from '@supabase/postgres-meta' +import pgMeta from '@supabase/pg-meta' +import { QueryClient, UseQueryOptions } from '@tanstack/react-query' +import { z } from 'zod' -import { get } from 'data/fetchers' -import type { ResponseError } from 'types' -import { databaseRolesKeys } from './keys' +import { ExecuteSqlData, useExecuteSqlQuery } from 'data/sql/execute-sql-query' +import { sqlKeys } from 'data/sql/keys' export type DatabaseRolesVariables = { projectRef?: string connectionString?: string } -export async function getDatabaseRoles( - { projectRef, connectionString }: DatabaseRolesVariables, - signal?: AbortSignal -) { - if (!projectRef) throw new Error('projectRef is required') +export type PgRole = z.infer - let headers = new Headers() - if (connectionString) headers.set('x-connection-encrypted', connectionString) +const pgMetaRolesList = pgMeta.roles.list() - const { data, error } = await get('/platform/pg-meta/{ref}/roles', { - params: { - header: { 'x-connection-encrypted': connectionString! }, - path: { ref: projectRef }, - }, - headers, - signal, - }) - - if (error) throw error - return data as PostgresRole[] -} - -export type DatabaseRolesData = Awaited> -export type DatabaseRolesError = ResponseError +export type DatabaseRolesData = z.infer +export type DatabaseRolesError = unknown export const useDatabaseRolesQuery = ( { projectRef, connectionString }: DatabaseRolesVariables, - { enabled = true, ...options }: UseQueryOptions = {} + options: UseQueryOptions = {} ) => - useQuery( - databaseRolesKeys.list(projectRef), - ({ signal }) => getDatabaseRoles({ projectRef, connectionString }, signal), + useExecuteSqlQuery( { - enabled: enabled && typeof projectRef !== 'undefined', + projectRef, + connectionString, + sql: pgMetaRolesList.sql, + queryKey: ['roles', 'list'], + }, + { + select(data) { + return data.result + }, ...options, } ) + +export function invalidateRolesQuery(client: QueryClient, projectRef: string | undefined) { + return client.invalidateQueries(sqlKeys.query(projectRef, ['roles', 'list'])) +} diff --git a/apps/studio/data/database-roles/keys.ts b/apps/studio/data/database-roles/keys.ts deleted file mode 100644 index 34ca7337bf4..00000000000 --- a/apps/studio/data/database-roles/keys.ts +++ /dev/null @@ -1,3 +0,0 @@ -export const databaseRolesKeys = { - list: (projectRef: string | undefined) => ['projects', projectRef, 'database-roles'] as const, -} diff --git a/apps/studio/data/database/schema-create-mutation.ts b/apps/studio/data/database/schema-create-mutation.ts index 1bd5a6cab6a..81499a34b97 100644 --- a/apps/studio/data/database/schema-create-mutation.ts +++ b/apps/studio/data/database/schema-create-mutation.ts @@ -3,7 +3,6 @@ import pgMeta from '@supabase/pg-meta' import { toast } from 'react-hot-toast' import type { ResponseError } from 'types' -import { databaseKeys } from './keys' import { executeSql } from 'data/sql/execute-sql-query' import { invalidateSchemasQuery } from './schemas-query' diff --git a/apps/studio/pages/project/[ref]/auth/column-privileges.tsx b/apps/studio/pages/project/[ref]/auth/column-privileges.tsx index b3aa68fb561..40d8c3d0d7b 100644 --- a/apps/studio/pages/project/[ref]/auth/column-privileges.tsx +++ b/apps/studio/pages/project/[ref]/auth/column-privileges.tsx @@ -28,7 +28,7 @@ import { useProjectContext } from 'components/layouts/ProjectLayout/ProjectConte import { ScaffoldContainer, ScaffoldSection } from 'components/layouts/Scaffold' import AlertError from 'components/ui/AlertError' import { GenericSkeletonLoader } from 'components/ui/ShimmeringLoader' -import { useDatabaseRolesQuery } from 'data/database-roles/database-roles-query' +import { PgRole, useDatabaseRolesQuery } from 'data/database-roles/database-roles-query' import { useColumnPrivilegesQuery } from 'data/privileges/column-privileges-query' import { useTablePrivilegesQuery } from 'data/privileges/table-privileges-query' import { useTablesQuery } from 'data/tables/tables-query' @@ -133,9 +133,8 @@ const PrivilegesPage: NextPageWithLayout = () => { [allColumnPrivileges, selectedRole, selectedSchema, selectedTable] ) - const rolesList = - allRoles?.filter((role: PostgresRole) => EDITABLE_ROLES.includes(role.name)) ?? [] - const roles = rolesList.map((role: PostgresRole) => role.name) + const rolesList = allRoles?.filter((role: PgRole) => EDITABLE_ROLES.includes(role.name)) ?? [] + const roles = rolesList.map((role: PgRole) => role.name) const table = tableList?.find( (table) => table.schema === selectedSchema && table.name === selectedTable diff --git a/apps/studio/styles/code.scss b/apps/studio/styles/code.scss index 3009884e0a3..6bc7f701e15 100644 --- a/apps/studio/styles/code.scss +++ b/apps/studio/styles/code.scss @@ -90,7 +90,10 @@ opacity: 0; position: absolute; visibility: hidden; - transition: opacity 200ms ease-in-out, visibility 200ms ease-in-out, bottom 200ms ease-in-out; + transition: + opacity 200ms ease-in-out, + visibility 200ms ease-in-out, + bottom 200ms ease-in-out; button { @apply rounded text-xs; diff --git a/apps/studio/styles/editor.scss b/apps/studio/styles/editor.scss index 4a76731d766..9c9236d76b3 100644 --- a/apps/studio/styles/editor.scss +++ b/apps/studio/styles/editor.scss @@ -27,4 +27,4 @@ input { @apply pr-10; } -} \ No newline at end of file +} diff --git a/apps/studio/styles/graphiql-base.scss b/apps/studio/styles/graphiql-base.scss index 1ef8ebdf1d9..eebcac28006 100644 --- a/apps/studio/styles/graphiql-base.scss +++ b/apps/studio/styles/graphiql-base.scss @@ -132,8 +132,7 @@ button.graphiql-tab-add > svg { /* The query editor and the toolbar */ .graphiql-container .graphiql-query-editor { - border-bottom: 1px solid - hsla(var(--color-neutral), var(--alpha-background-heavy)); + border-bottom: 1px solid hsla(var(--color-neutral), var(--alpha-background-heavy)); padding: var(--px-16); column-gap: var(--px-16); display: flex; @@ -175,9 +174,7 @@ button.graphiql-tab-add > svg { } /* The tab buttons to switch between editor tools */ -.graphiql-container - .graphiql-editor-tools - > button:not(.graphiql-toggle-editor-tools) { +.graphiql-container .graphiql-editor-tools > button:not(.graphiql-toggle-editor-tools) { padding: var(--px-8) var(--px-12); } @@ -219,14 +216,12 @@ button.graphiql-tab-add > svg { /* The footer below the response view */ .graphiql-container .graphiql-footer { - border-top: 1px solid - hsla(var(--color-neutral), var(--alpha-background-heavy)); + border-top: 1px solid hsla(var(--color-neutral), var(--alpha-background-heavy)); } /* The plugin container */ .graphiql-container .graphiql-plugin { - border-left: 1px solid - hsla(var(--color-neutral), var(--alpha-background-heavy)); + border-left: 1px solid hsla(var(--color-neutral), var(--alpha-background-heavy)); flex: 1; overflow-y: auto; padding: var(--px-16); @@ -239,8 +234,7 @@ button.graphiql-tab-add > svg { } .graphiql-horizontal-drag-bar:hover::after { - border: var(--px-2) solid - hsla(var(--color-neutral), var(--alpha-background-heavy)); + border: var(--px-2) solid hsla(var(--color-neutral), var(--alpha-background-heavy)); border-radius: var(--border-radius-2); content: ''; display: block; @@ -292,8 +286,7 @@ button.graphiql-tab-add > svg { /* A section inside the settings dialog */ .graphiql-dialog .graphiql-dialog-section { align-items: center; - border-top: 1px solid - hsla(var(--color-neutral), var(--alpha-background-heavy)); + border-top: 1px solid hsla(var(--color-neutral), var(--alpha-background-heavy)); display: flex; justify-content: space-between; padding: var(--px-24); diff --git a/apps/studio/styles/markdown-preview.scss b/apps/studio/styles/markdown-preview.scss index 742983e6b76..07e97c62972 100644 --- a/apps/studio/styles/markdown-preview.scss +++ b/apps/studio/styles/markdown-preview.scss @@ -2,7 +2,7 @@ @media (prefers-color-scheme: dark) { .markdown-body, - [data-theme="dark"] { + [data-theme='dark'] { /*dark*/ color-scheme: dark; --color-prettylights-syntax-comment: #8b949e; @@ -42,14 +42,14 @@ --color-canvas-subtle: #161b22; --color-border-default: #30363d; --color-border-muted: #21262d; - --color-neutral-muted: rgba(110,118,129,0.4); + --color-neutral-muted: rgba(110, 118, 129, 0.4); --color-accent-fg: #2f81f7; --color-accent-emphasis: #1f6feb; --color-success-fg: #3fb950; --color-success-emphasis: #238636; --color-attention-fg: #d29922; --color-attention-emphasis: #9e6a03; - --color-attention-subtle: rgba(187,128,9,0.15); + --color-attention-subtle: rgba(187, 128, 9, 0.15); --color-danger-fg: #f85149; --color-danger-emphasis: #da3633; --color-done-fg: #a371f7; @@ -59,7 +59,7 @@ @media (prefers-color-scheme: light) { .markdown-body, - [data-theme="light"] { + [data-theme='light'] { /*light*/ color-scheme: light; --color-prettylights-syntax-comment: #57606a; @@ -92,14 +92,14 @@ --color-prettylights-syntax-brackethighlighter-angle: #57606a; --color-prettylights-syntax-sublimelinter-gutter-mark: #8c959f; --color-prettylights-syntax-constant-other-reference-link: #0a3069; - --color-fg-default: #1F2328; + --color-fg-default: #1f2328; --color-fg-muted: #656d76; --color-fg-subtle: #6e7781; --color-canvas-default: #ffffff; --color-canvas-subtle: #f6f8fa; --color-border-default: #d0d7de; - --color-border-muted: hsla(210,18%,87%,1); - --color-neutral-muted: rgba(175,184,193,0.2); + --color-border-muted: hsla(210, 18%, 87%, 1); + --color-neutral-muted: rgba(175, 184, 193, 0.2); --color-accent-fg: #0969da; --color-accent-emphasis: #0969da; --color-success-fg: #1a7f37; @@ -118,7 +118,8 @@ -ms-text-size-adjust: 100%; -webkit-text-size-adjust: 100%; margin: 0; - font-family: -apple-system,BlinkMacSystemFont,"Segoe UI","Noto Sans",Helvetica,Arial,sans-serif,"Apple Color Emoji","Segoe UI Emoji"; + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Noto Sans', Helvetica, Arial, + sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji'; font-size: 16px; line-height: 1.5; word-wrap: break-word; @@ -181,9 +182,9 @@ } .markdown-body h1 { - margin: .67em 0; + margin: 0.67em 0; font-weight: var(--base-text-weight-semibold, 600); - padding-bottom: .3em; + padding-bottom: 0.3em; font-size: 2em; @apply border-b; } @@ -237,7 +238,7 @@ overflow: hidden; background: transparent; @apply border-b; - height: .25em; + height: 0.25em; padding: 0; margin: 24px 0; background-color: var(--color-border-default); @@ -253,33 +254,33 @@ line-height: inherit; } -.markdown-body [type=button], -.markdown-body [type=reset], -.markdown-body [type=submit] { +.markdown-body [type='button'], +.markdown-body [type='reset'], +.markdown-body [type='submit'] { -webkit-appearance: button; appearance: button; } -.markdown-body [type=checkbox], -.markdown-body [type=radio] { +.markdown-body [type='checkbox'], +.markdown-body [type='radio'] { box-sizing: border-box; padding: 0; } -.markdown-body [type=number]::-webkit-inner-spin-button, -.markdown-body [type=number]::-webkit-outer-spin-button { +.markdown-body [type='number']::-webkit-inner-spin-button, +.markdown-body [type='number']::-webkit-outer-spin-button { height: auto; } -.markdown-body [type=search]::-webkit-search-cancel-button, -.markdown-body [type=search]::-webkit-search-decoration { +.markdown-body [type='search']::-webkit-search-cancel-button, +.markdown-body [type='search']::-webkit-search-decoration { -webkit-appearance: none; appearance: none; } .markdown-body ::-webkit-input-placeholder { color: inherit; - opacity: .54; + opacity: 0.54; } .markdown-body ::-webkit-file-upload-button { @@ -299,13 +300,13 @@ .markdown-body hr::before { display: table; - content: ""; + content: ''; } .markdown-body hr::after { display: table; clear: both; - content: ""; + content: ''; } .markdown-body table { @@ -326,30 +327,30 @@ cursor: pointer; } -.markdown-body details:not([open])>*:not(summary) { +.markdown-body details:not([open]) > *:not(summary) { display: none !important; } .markdown-body a:focus, -.markdown-body [role=button]:focus, -.markdown-body input[type=radio]:focus, -.markdown-body input[type=checkbox]:focus { +.markdown-body [role='button']:focus, +.markdown-body input[type='radio']:focus, +.markdown-body input[type='checkbox']:focus { outline: 2px solid var(--color-accent-fg); outline-offset: -2px; box-shadow: none; } .markdown-body a:focus:not(:focus-visible), -.markdown-body [role=button]:focus:not(:focus-visible), -.markdown-body input[type=radio]:focus:not(:focus-visible), -.markdown-body input[type=checkbox]:focus:not(:focus-visible) { +.markdown-body [role='button']:focus:not(:focus-visible), +.markdown-body input[type='radio']:focus:not(:focus-visible), +.markdown-body input[type='checkbox']:focus:not(:focus-visible) { outline: solid 1px transparent; } .markdown-body a:focus-visible, -.markdown-body [role=button]:focus-visible, -.markdown-body input[type=radio]:focus-visible, -.markdown-body input[type=checkbox]:focus-visible { +.markdown-body [role='button']:focus-visible, +.markdown-body input[type='radio']:focus-visible, +.markdown-body input[type='checkbox']:focus-visible { outline: 2px solid var(--color-accent-fg); outline-offset: -2px; box-shadow: none; @@ -357,17 +358,24 @@ .markdown-body a:not([class]):focus, .markdown-body a:not([class]):focus-visible, -.markdown-body input[type=radio]:focus, -.markdown-body input[type=radio]:focus-visible, -.markdown-body input[type=checkbox]:focus, -.markdown-body input[type=checkbox]:focus-visible { +.markdown-body input[type='radio']:focus, +.markdown-body input[type='radio']:focus-visible, +.markdown-body input[type='checkbox']:focus, +.markdown-body input[type='checkbox']:focus-visible { outline-offset: 0; } .markdown-body kbd { display: inline-block; padding: 3px 5px; - font: 11px ui-monospace,SFMono-Regular,SF Mono,Menlo,Consolas,Liberation Mono,monospace; + font: + 11px ui-monospace, + SFMono-Regular, + SF Mono, + Menlo, + Consolas, + Liberation Mono, + monospace; line-height: 10px; color: var(--color-fg-default); vertical-align: middle; @@ -392,7 +400,7 @@ .markdown-body h2 { font-weight: var(--base-text-weight-semibold, 600); - padding-bottom: .3em; + padding-bottom: 0.3em; font-size: 1.5em; @apply border-b; } @@ -409,12 +417,12 @@ .markdown-body h5 { font-weight: var(--base-text-weight-semibold, 600); - font-size: .875em; + font-size: 0.875em; } .markdown-body h6 { font-weight: var(--base-text-weight-semibold, 600); - font-size: .85em; + font-size: 0.85em; color: var(--color-fg-muted); } @@ -427,7 +435,7 @@ margin: 0; padding: 0 1em; color: var(--color-fg-muted); - border-left: .25em solid var(--color-border-default); + border-left: 0.25em solid var(--color-border-default); } .markdown-body ul, @@ -456,14 +464,28 @@ .markdown-body tt, .markdown-body code, .markdown-body samp { - font-family: ui-monospace,SFMono-Regular,SF Mono,Menlo,Consolas,Liberation Mono,monospace; + font-family: + ui-monospace, + SFMono-Regular, + SF Mono, + Menlo, + Consolas, + Liberation Mono, + monospace; font-size: 12px; } .markdown-body pre { margin-top: 0; margin-bottom: 0; - font-family: ui-monospace,SFMono-Regular,SF Mono,Menlo,Consolas,Liberation Mono,monospace; + font-family: + ui-monospace, + SFMono-Regular, + SF Mono, + Menlo, + Consolas, + Liberation Mono, + monospace; font-size: 12px; word-wrap: normal; } @@ -488,20 +510,20 @@ .markdown-body::before { display: table; - content: ""; + content: ''; } .markdown-body::after { display: table; clear: both; - content: ""; + content: ''; } -.markdown-body>*:first-child { +.markdown-body > *:first-child { margin-top: 0 !important; } -.markdown-body>*:last-child { +.markdown-body > *:last-child { margin-bottom: 0 !important; } @@ -537,11 +559,11 @@ margin-bottom: 16px; } -.markdown-body blockquote>:first-child { +.markdown-body blockquote > :first-child { margin-top: 0; } -.markdown-body blockquote>:last-child { +.markdown-body blockquote > :last-child { margin-bottom: 0; } @@ -586,7 +608,7 @@ .markdown-body h5 code, .markdown-body h6 tt, .markdown-body h6 code { - padding: 0 .2em; + padding: 0 0.2em; font-size: inherit; } @@ -620,27 +642,27 @@ list-style-type: none; } -.markdown-body ol[type="a s"] { +.markdown-body ol[type='a s'] { list-style-type: lower-alpha; } -.markdown-body ol[type="A s"] { +.markdown-body ol[type='A s'] { list-style-type: upper-alpha; } -.markdown-body ol[type="i s"] { +.markdown-body ol[type='i s'] { list-style-type: lower-roman; } -.markdown-body ol[type="I s"] { +.markdown-body ol[type='I s'] { list-style-type: upper-roman; } -.markdown-body ol[type="1"] { +.markdown-body ol[type='1'] { list-style-type: decimal; } -.markdown-body div>ol:not([type]) { +.markdown-body div > ol:not([type]) { list-style-type: decimal; } @@ -652,12 +674,12 @@ margin-bottom: 0; } -.markdown-body li>p { +.markdown-body li > p { margin-top: 16px; } -.markdown-body li+li { - margin-top: .25em; +.markdown-body li + li { + margin-top: 0.25em; } .markdown-body dl { @@ -687,7 +709,7 @@ border: 1px solid var(--color-border-default); } -.markdown-body table td>:last-child { +.markdown-body table td > :last-child { margin-bottom: 0; } @@ -704,11 +726,11 @@ background-color: transparent; } -.markdown-body img[align=right] { +.markdown-body img[align='right'] { padding-left: 20px; } -.markdown-body img[align=left] { +.markdown-body img[align='left'] { padding-right: 20px; } @@ -723,7 +745,7 @@ overflow: hidden; } -.markdown-body span.frame>span { +.markdown-body span.frame > span { display: block; float: left; width: auto; @@ -751,7 +773,7 @@ clear: both; } -.markdown-body span.align-center>span { +.markdown-body span.align-center > span { display: block; margin: 13px auto 0; overflow: hidden; @@ -769,7 +791,7 @@ clear: both; } -.markdown-body span.align-right>span { +.markdown-body span.align-right > span { display: block; margin: 13px 0 0; overflow: hidden; @@ -799,7 +821,7 @@ overflow: hidden; } -.markdown-body span.float-right>span { +.markdown-body span.float-right > span { display: block; margin: 13px auto 0; overflow: hidden; @@ -808,7 +830,7 @@ .markdown-body code, .markdown-body tt { - padding: .2em .4em; + padding: 0.2em 0.4em; margin: 0; font-size: 85%; white-space: break-spaces; @@ -833,7 +855,7 @@ font-size: 100%; } -.markdown-body pre>code { +.markdown-body pre > code { padding: 0; margin: 0; word-break: normal; @@ -903,11 +925,11 @@ } .markdown-body [data-footnote-ref]::before { - content: "["; + content: '['; } .markdown-body [data-footnote-ref]::after { - content: "]"; + content: ']'; } .markdown-body .footnotes { @@ -937,7 +959,7 @@ bottom: -8px; left: -24px; pointer-events: none; - content: ""; + content: ''; border: 2px solid var(--color-accent-emphasis); border-radius: 6px; } @@ -1073,7 +1095,7 @@ .markdown-body g-emoji { display: inline-block; min-width: 1ch; - font-family: "Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol"; + font-family: 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; font-size: 1em; font-style: normal !important; font-weight: var(--base-text-weight-normal, 400); @@ -1098,7 +1120,7 @@ cursor: pointer; } -.markdown-body .task-list-item+.task-list-item { +.markdown-body .task-list-item + .task-list-item { margin-top: 4px; } @@ -1107,12 +1129,12 @@ } .markdown-body .task-list-item-checkbox { - margin: 0 .2em .25em -1.4em; + margin: 0 0.2em 0.25em -1.4em; vertical-align: middle; } .markdown-body .contains-task-list:dir(rtl) .task-list-item-checkbox { - margin: 0 -1.6em .25em .2em; + margin: 0 -1.6em 0.25em 0.2em; } .markdown-body .contains-task-list { @@ -1136,14 +1158,14 @@ padding: var(--base-size-8) var(--base-size-16); margin-bottom: 16px; color: inherit; - border-left: .25em solid var(--color-border-default); + border-left: 0.25em solid var(--color-border-default); } -.markdown-body .markdown-alert>:first-child { +.markdown-body .markdown-alert > :first-child { margin-top: 0; } -.markdown-body .markdown-alert>:last-child { +.markdown-body .markdown-alert > :last-child { margin-bottom: 0; } @@ -1192,4 +1214,4 @@ .markdown-body .markdown-alert.markdown-alert-caution .markdown-alert-title { color: var(--color-danger-fg); -} \ No newline at end of file +} diff --git a/apps/studio/styles/reactflow.scss b/apps/studio/styles/reactflow.scss index 083b194f818..3907847eae9 100644 --- a/apps/studio/styles/reactflow.scss +++ b/apps/studio/styles/reactflow.scss @@ -15,4 +15,4 @@ to { stroke-dashoffset: 100; } -} \ No newline at end of file +} diff --git a/apps/studio/styles/toast.scss b/apps/studio/styles/toast.scss index 03e7719f9a7..4463dff48b1 100644 --- a/apps/studio/styles/toast.scss +++ b/apps/studio/styles/toast.scss @@ -64,7 +64,9 @@ margin-bottom: 1rem; padding: 8px; border-radius: 1px; - box-shadow: 0 1px 10px 0 rgba(0, 0, 0, 0.1), 0 2px 15px 0 rgba(0, 0, 0, 0.05); + box-shadow: + 0 1px 10px 0 rgba(0, 0, 0, 0.1), + 0 2px 15px 0 rgba(0, 0, 0, 0.05); display: -ms-flexbox; display: flex; -ms-flex-pack: justify; @@ -559,4 +561,4 @@ } } -/*# sourceMappingURL=ReactToastify.css.map */ \ No newline at end of file +/*# sourceMappingURL=ReactToastify.css.map */ diff --git a/packages/pg-meta/src/index.ts b/packages/pg-meta/src/index.ts index f1262d58ceb..aaadfe36832 100644 --- a/packages/pg-meta/src/index.ts +++ b/packages/pg-meta/src/index.ts @@ -1,5 +1,7 @@ +import roles from './pg-meta-roles' import schemas from './pg-meta-schemas' export default { + roles, schemas, } diff --git a/packages/pg-meta/src/pg-meta-roles.ts b/packages/pg-meta/src/pg-meta-roles.ts new file mode 100644 index 00000000000..f3c5ca391c5 --- /dev/null +++ b/packages/pg-meta/src/pg-meta-roles.ts @@ -0,0 +1,253 @@ +import { ident, literal } from 'pg-format' +import { ROLES_SQL } from './sql/roles' +import { z } from 'zod' + +const pgRoleZod = z.object({ + id: z.number(), + name: z.string(), + isSuperuser: z.boolean(), + canCreateDb: z.boolean(), + canCreateRole: z.boolean(), + inheritRole: z.boolean(), + canLogin: z.boolean(), + isReplicationRole: z.boolean(), + canBypassRls: z.boolean(), + activeConnections: z.number(), + connectionLimit: z.number(), + validUntil: z.union([z.string(), z.null()]), + config: z.record(z.string(), z.string()), +}) +const pgRoleArrayZod = z.array(pgRoleZod) +const pgRoleOptionalZod = z.optional(pgRoleZod) + +function list({ + includeDefaultRoles: includeDefaultRoles = false, + limit, + offset, +}: { + includeDefaultRoles?: boolean + limit?: number + offset?: number +} = {}): { + sql: string + zod: typeof pgRoleArrayZod +} { + let sql = ` +with + roles as (${ROLES_SQL}) +select + * +from + roles +where + true +` + if (!includeDefaultRoles) { + // All default/predefined roles start with pg_: https://www.postgresql.org/docs/15/predefined-roles.html + // The pg_ prefix is also reserved: + // + // ``` + // postgres=# create role pg_myrole; + // ERROR: role name "pg_myrole" is reserved + // DETAIL: Role names starting with "pg_" are reserved. + // ``` + sql += ` and not pg_catalog.starts_with(name, 'pg_')` + } + if (limit) { + sql += ` limit ${limit}` + } + if (offset) { + sql += ` offset ${offset}` + } + return { + sql, + zod: pgRoleArrayZod, + } +} + +function retrieve({ id }: { id: number }): { sql: string; zod: typeof pgRoleOptionalZod } +function retrieve({ name }: { name: string }): { sql: string; zod: typeof pgRoleOptionalZod } +function retrieve({ id, name }: { id?: number; name?: string }): { sql: string; zod: typeof pgRoleOptionalZod } { + if (id) { + const sql = `${ROLES_SQL} where r.oid = ${literal(id)};` + return { + sql, + zod: pgRoleOptionalZod, + } + } else { + const sql = `${ROLES_SQL} where rolname = ${literal(name)};` + return { + sql, + zod: pgRoleOptionalZod, + } + } +} + +type RoleCreateParams = { + name: string, + isSuperuser?: boolean, + canCreateDb?: boolean, + canCreateRole?: boolean, + inheritRole?: boolean, + canLogin?: boolean, + isReplicationRole?: boolean, + canBypassRls?: boolean, + connectionLimit?: number, + password?: string, + validUntil?: string, + memberOf?: string[], + members?: string[], + admins?: string[], + config?: Record, +} +function create({ + name, + isSuperuser = false, + canCreateDb = false, + canCreateRole = false, + inheritRole = true, + canLogin = false, + isReplicationRole = false, + canBypassRls = false, + connectionLimit = -1, + password, + validUntil, + memberOf = [], + members = [], + admins = [], + config = {}, +}: RoleCreateParams): { sql: string } { + const sql = ` +create role ${ident(name)} + ${isSuperuser ? 'superuser' : ''} + ${canCreateDb ? 'createdb' : ''} + ${canCreateRole ? 'createrole' : ''} + ${inheritRole ? '' : 'noinherit'} + ${canLogin ? 'login' : ''} + ${isReplicationRole ? 'replication' : ''} + ${canBypassRls ? 'bypassrls' : ''} + connection limit ${connectionLimit} + ${password === undefined ? '' : `password ${literal(password)}`} + ${validUntil === undefined ? '' : `valid until ${literal(validUntil)}`} + ${memberOf.length === 0 ? '' : `in role ${memberOf.map(ident).join(',')}`} + ${members.length === 0 ? '' : `role ${members.map(ident).join(',')}`} + ${admins.length === 0 ? '' : `admin ${admins.map(ident).join(',')}`} + ; +${Object.entries(config).map(([param, value]) => `alter role ${ident(name)} set ${ident(param)} = ${literal(value)};`).join('\n')} +` + return { sql } +} + +type RoleUpdateParams = { + name?: string, + isSuperuser?: boolean, + canCreateDb?: boolean, + canCreateRole?: boolean, + inheritRole?: boolean, + canLogin?: boolean, + isReplicationRole?: boolean, + canBypassRls?: boolean, + connectionLimit?: number, + password?: string, + validUntil?: string, +} +function update({ id }: { id: number }, params: RoleUpdateParams): { sql: string } +function update({ name }: { name: string }, params: RoleUpdateParams): { sql: string } +function update( + { + id, + name, + }: { + id?: number + name?: string + }, + { + name: newName, + isSuperuser, + canCreateDb, + canCreateRole, + inheritRole, + canLogin, + isReplicationRole, + canBypassRls, + connectionLimit, + password, + validUntil, + }: RoleUpdateParams): { sql: string } { + const sql = ` +do $$ +declare + id oid := ${id === undefined ? `${literal(name)}::regrole` : literal(id)}; + old record; +begin + select * into old from pg_roles where oid = id; + if old is null then + raise exception 'Cannot find role with id %', id; + end if; + + execute(format('alter role %I + ${isSuperuser === undefined ? '' : isSuperuser ? 'superuser' : 'nosuperuser'} + ${canCreateDb === undefined ? '' : canCreateDb ? 'createdb' : 'nocreatedb'} + ${canCreateRole === undefined ? '' : canCreateRole ? 'createrole' : 'nocreaterole'} + ${inheritRole === undefined ? '' : inheritRole ? 'inherit' : 'noinherit'} + ${canLogin === undefined ? '' : canLogin ? 'login' : 'nologin'} + ${isReplicationRole === undefined ? '' : isReplicationRole ? 'replication' : 'noreplication'} + ${canBypassRls === undefined ? '' : canBypassRls ? 'bypassrls' : 'nobypassrls'} + ${connectionLimit === undefined ? '' : `connection limit ${literal(connectionLimit)}`} + ${password === undefined ? '' : `password ${literal(password)}`} + ${validUntil === undefined ? '' : `valid until ${literal(validUntil)}`} + ', old.rolname)); + + ${newName === undefined ? '' : ` + -- Using the same name in the rename clause gives an error, so only do it if the new name is different. + if new_name != old.nspname then + execute(format('alter role %I rename to ${ident(newName)};', old.nspname)); + end if; + `} +end +$$; +` + return { sql } +} + +type RoleRemoveParams = { + ifExists?: boolean +} +function remove({ id }: { id: number }, params?: RoleRemoveParams): { sql: string } +function remove({ name }: { name: string }, params?: RoleRemoveParams): { sql: string } +function remove( + { + id, + name, + }: { + id?: number + name?: string + }, + { ifExists = false }: RoleRemoveParams = {} +): { sql: string } { + const sql = ` +do $$ +declare + id oid := ${id === undefined ? `${literal(name)}::regrole` : literal(id)}; + old record; +begin + select * into old from pg_roles where oid = id; + if old is null then + raise exception 'Cannot find role with id %', id; + end if; + + execute(format('drop role ${ifExists ? 'if exists' : ''} %I;', old.rolname)); +end +$$; +` + return { sql } +} + +export default { + list, + retrieve, + create, + update, + remove, + zod: pgRoleZod, +} diff --git a/packages/pg-meta/src/sql/roles.ts b/packages/pg-meta/src/sql/roles.ts new file mode 100644 index 00000000000..088691f547d --- /dev/null +++ b/packages/pg-meta/src/sql/roles.ts @@ -0,0 +1,46 @@ +export const ROLES_SQL = /* SQL */ ` +-- Can't use pg_authid here since some managed Postgres providers don't expose it +-- https://github.com/supabase/postgres-meta/issues/212 + +select + r.oid :: int8 as id, + rolname as name, + rolsuper as "isSuperuser", + rolcreatedb as "canCreateDb", + rolcreaterole as "canCreateRole", + rolinherit as "inheritRole", + rolcanlogin as "canLogin", + rolreplication as "isReplicationRole", + rolbypassrls as "canBypassRls", + ( + select + count(*) + from + pg_stat_activity + where + r.rolname = pg_stat_activity.usename + ) as "activeConnections", + case when rolconnlimit = -1 then current_setting('max_connections') :: int8 + else rolconnlimit + end as "connectionLimit", + rolvaliduntil as "validUntil", + coalesce(r_config.role_configs, '{}') as config +from + pg_roles r + left join ( + select + oid, + jsonb_object_agg(param, value) filter (where param is not null) as role_configs + from + ( + select + oid, + (string_to_array(unnest(rolconfig), '='))[1] as param, + (string_to_array(unnest(rolconfig), '='))[2] as value + from + pg_roles + ) as _ + group by + oid + ) r_config on r_config.oid = r.oid +`