chore: use @supabase/pg-meta for role queries (#22821)

* chore: use @supabase/pg-meta for role queries

* chore: prettier

* fix: typo
This commit is contained in:
Bobbie Soedirgo
2024-04-17 16:19:49 +07:00
committed by GitHub
parent aa7770d1be
commit 1d58f2a11b
22 changed files with 533 additions and 242 deletions

View File

@@ -43,7 +43,7 @@ const PolicyRoles = ({ selectedRoles, onUpdateSelectedRoles }: PolicyRolesProps)
</div>
<div className="relative w-2/3">
{isLoading && <ShimmeringLoader className="py-4" />}
{isError && <AlertError error={error} subject="Failed to retrieve database roles" />}
{isError && <AlertError error={error as any} subject="Failed to retrieve database roles" />}
{isSuccess && (
<MultiSelect
options={formattedRoles}

View File

@@ -26,22 +26,22 @@ interface CreateRolePanelProps {
const FormSchema = z.object({
name: z.string().trim().min(1, 'You must provide a name').default(''),
is_superuser: z.boolean().default(false),
can_login: z.boolean().default(false),
can_create_role: z.boolean().default(false),
can_create_db: z.boolean().default(false),
is_replication_role: z.boolean().default(false),
can_bypass_rls: z.boolean().default(false),
isSuperuser: z.boolean().default(false),
canLogin: z.boolean().default(false),
canCreateRole: z.boolean().default(false),
canCreateDb: z.boolean().default(false),
isReplicationRole: z.boolean().default(false),
canBypassRls: z.boolean().default(false),
})
const initialValues = {
name: '',
is_superuser: false,
can_login: false,
can_create_role: false,
can_create_db: false,
is_replication_role: false,
can_bypass_rls: false,
isSuperuser: false,
canLogin: false,
canCreateRole: false,
canCreateDb: false,
isReplicationRole: false,
canBypassRls: false,
}
const CreateRolePanel = ({ visible, onClose }: CreateRolePanelProps) => {
@@ -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()
},
})

View File

@@ -27,7 +27,7 @@ const DeleteRoleModal = ({ role, visible, onClose }: DeleteRoleModalProps) => {
deleteDatabaseRole({
projectRef: project.ref,
connectionString: project.connectionString,
id: role.id.toString(),
id: role.id,
})
}

View File

@@ -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<PgRole>, { 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) => {
<Form
name="role-update-form"
initialValues={{
is_superuser,
can_login,
can_create_role,
can_create_db,
is_replication_role,
can_bypass_rls,
isSuperuser,
canLogin,
canCreateRole,
canCreateDb,
isReplicationRole,
canBypassRls,
}}
onSubmit={onSaveChanges}
className={[
@@ -118,7 +115,7 @@ const RoleRow = ({ role, disabled = false, onSelectDelete }: RoleRowProps) => {
</div>
</div>
<div className="flex items-center space-x-4">
{role.active_connections > 0 && (
{role.activeConnections > 0 && (
<div className="relative h-2 w-2">
<span className="flex h-2 w-2">
<span className="absolute inline-flex h-full w-full animate-ping rounded-full bg-brand opacity-75"></span>
@@ -129,10 +126,10 @@ const RoleRow = ({ role, disabled = false, onSelectDelete }: RoleRowProps) => {
<p
id="collapsible-trigger"
className={`text-sm ${
role.active_connections > 0 ? 'text-foreground' : 'text-foreground-light'
role.activeConnections > 0 ? 'text-foreground' : 'text-foreground-light'
}`}
>
{role.active_connections} connections
{role.activeConnections} connections
</p>
{!disabled && (
<DropdownMenu>

View File

@@ -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',

View File

@@ -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 = () => {
<p className="text-xs text-foreground-light pr-2">Connections by roles:</p>
{rolesWithActiveConnections.map((role) => (
<div key={role.id} className="text-xs text-foreground">
{role.name}: {role.active_connections}
{role.name}: {role.activeConnections}
</div>
))}
</div>

View File

@@ -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<typeof pgMeta.roles.create>[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<ReturnType<typeof createDatabaseRole>>
@@ -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) {

View File

@@ -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<typeof pgMeta.roles.remove>[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<ReturnType<typeof deleteDatabaseRole>>
@@ -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) {

View File

@@ -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<typeof pgMeta.roles.update>[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<ReturnType<typeof updateDatabaseRole>>
@@ -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) {

View File

@@ -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<typeof pgMeta.roles.zod>
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<ReturnType<typeof getDatabaseRoles>>
export type DatabaseRolesError = ResponseError
export type DatabaseRolesData = z.infer<typeof pgMetaRolesList.zod>
export type DatabaseRolesError = unknown
export const useDatabaseRolesQuery = <TData = DatabaseRolesData>(
{ projectRef, connectionString }: DatabaseRolesVariables,
{ enabled = true, ...options }: UseQueryOptions<DatabaseRolesData, DatabaseRolesError, TData> = {}
options: UseQueryOptions<ExecuteSqlData, DatabaseRolesError, TData> = {}
) =>
useQuery<DatabaseRolesData, DatabaseRolesError, TData>(
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']))
}

View File

@@ -1,3 +0,0 @@
export const databaseRolesKeys = {
list: (projectRef: string | undefined) => ['projects', projectRef, 'database-roles'] as const,
}

View File

@@ -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'

View File

@@ -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

View File

@@ -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;

View File

@@ -27,4 +27,4 @@
input {
@apply pr-10;
}
}
}

View File

@@ -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);

View File

@@ -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);
}
}

View File

@@ -15,4 +15,4 @@
to {
stroke-dashoffset: 100;
}
}
}

View File

@@ -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 */
/*# sourceMappingURL=ReactToastify.css.map */

View File

@@ -1,5 +1,7 @@
import roles from './pg-meta-roles'
import schemas from './pg-meta-schemas'
export default {
roles,
schemas,
}

View File

@@ -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<string, string>,
}
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,
}

View File

@@ -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
`