diff --git a/apps/docs/content/guides/database/tables.mdx b/apps/docs/content/guides/database/tables.mdx index 6d2725ceaff74..f9105daeeb684 100644 --- a/apps/docs/content/guides/database/tables.mdx +++ b/apps/docs/content/guides/database/tables.mdx @@ -523,7 +523,7 @@ create view with(security_invoker=true) as ( ### When to use views -Views provide the several benefits: +Views provide several benefits: - Simplicity - Consistency diff --git a/apps/studio/components/interfaces/Auth/Policies/PolicyTableRow/index.tsx b/apps/studio/components/interfaces/Auth/Policies/PolicyTableRow/index.tsx index 47f460e55273e..b8d9c8f3db47d 100644 --- a/apps/studio/components/interfaces/Auth/Policies/PolicyTableRow/index.tsx +++ b/apps/studio/components/interfaces/Auth/Policies/PolicyTableRow/index.tsx @@ -4,7 +4,7 @@ import { noop } from 'lodash' import AlertError from 'components/ui/AlertError' import { useProjectPostgrestConfigQuery } from 'data/config/project-postgrest-config-query' import { useDatabasePoliciesQuery } from 'data/database-policies/database-policies-query' -import { useTableRolesAccessQuery } from 'data/tables/table-roles-access-query' +import { useTablesRolesAccessQuery } from 'data/tables/tables-roles-access-query' import { useSelectedProjectQuery } from 'hooks/misc/useSelectedProject' import { Alert_Shadcn_, @@ -65,13 +65,13 @@ export const PolicyTableRow = ({ const isRLSEnabled = table.rls_enabled const isTableExposedThroughAPI = exposedSchemas.includes(table.schema) - const { data: roles = [] } = useTableRolesAccessQuery({ + const { data: tablesWithAnonAuthAccess = new Set() } = useTablesRolesAccessQuery({ projectRef: project?.ref, connectionString: project?.connectionString, schema: table.schema, - table: table.name, }) - const hasAnonAuthenticatedRolesAccess = roles.length !== 0 + + const hasAnonAuthenticatedRolesAccess = tablesWithAnonAuthAccess.has(table.name) const isPubliclyReadableWritable = !isRLSEnabled && isTableExposedThroughAPI && hasAnonAuthenticatedRolesAccess diff --git a/apps/studio/components/interfaces/Organization/NewOrg/NewOrgForm.tsx b/apps/studio/components/interfaces/Organization/NewOrg/NewOrgForm.tsx index ee6fd9f90a493..20e307dc5aaba 100644 --- a/apps/studio/components/interfaces/Organization/NewOrg/NewOrgForm.tsx +++ b/apps/studio/components/interfaces/Organization/NewOrg/NewOrgForm.tsx @@ -299,7 +299,6 @@ export const NewOrgForm = ({ const paymentRef = useRef(null) const onSubmit: SubmitHandler> = async (formValues) => { - debugger const hasFreeOrgWithProjects = freeOrgs.some((it) => projectsByOrg[it.slug]?.length > 0) if (hasFreeOrgWithProjects && formValues.plan !== 'FREE') { diff --git a/apps/studio/components/interfaces/Organization/NewProject/FreeProjectLimitWarning.tsx b/apps/studio/components/interfaces/Organization/NewProject/FreeProjectLimitWarning.tsx index 6411fbe45712b..4aa43e5937ccc 100644 --- a/apps/studio/components/interfaces/Organization/NewProject/FreeProjectLimitWarning.tsx +++ b/apps/studio/components/interfaces/Organization/NewProject/FreeProjectLimitWarning.tsx @@ -1,18 +1,12 @@ -import Link from 'next/link' - +import { UpgradePlanButton } from 'components/ui/UpgradePlanButton' import type { MemberWithFreeProjectLimit } from 'data/organizations/free-project-limit-check-query' -import { Button } from 'ui' import { Admonition } from 'ui-patterns/admonition' interface FreeProjectLimitWarningProps { membersExceededLimit: MemberWithFreeProjectLimit[] - orgSlug: string } -const FreeProjectLimitWarning = ({ - membersExceededLimit, - orgSlug, -}: FreeProjectLimitWarningProps) => { +export const FreeProjectLimitWarning = ({ membersExceededLimit }: FreeProjectLimitWarningProps) => { return ( -
- -
+ Upgrade plan } /> ) } - -export default FreeProjectLimitWarning diff --git a/apps/studio/components/interfaces/Organization/NewProject/index.ts b/apps/studio/components/interfaces/Organization/NewProject/index.ts index 2b5935378f5b4..a52ded23d6bcc 100644 --- a/apps/studio/components/interfaces/Organization/NewProject/index.ts +++ b/apps/studio/components/interfaces/Organization/NewProject/index.ts @@ -1,2 +1 @@ export { default as NotOrganizationOwnerWarning } from './NotOrganizationOwnerWarning' -export { default as FreeProjectLimitWarning } from './FreeProjectLimitWarning' diff --git a/apps/studio/components/interfaces/Organization/TeamSettings/InviteMemberButton.tsx b/apps/studio/components/interfaces/Organization/TeamSettings/InviteMemberButton.tsx index 6872cf036db85..c96b767e6ce75 100644 --- a/apps/studio/components/interfaces/Organization/TeamSettings/InviteMemberButton.tsx +++ b/apps/studio/components/interfaces/Organization/TeamSettings/InviteMemberButton.tsx @@ -256,6 +256,7 @@ export const InviteMemberButton = () => { > { const { data: selectedOrganization } = useSelectedOrganizationQuery() const { data: permissions } = usePermissionsQuery() - const { data: members } = useOrganizationMembersQuery({ slug }) - const { data: allRoles } = useOrganizationRolesV2Query({ slug }) - const hasProjectScopedRoles = (allRoles?.project_scoped_roles ?? []).length > 0 + const { data: members } = useOrganizationMembersQuery({ slug }) + const isOptedIntoProjectLevelPermissions = useHasAccessToProjectLevelPermissions(slug as string) // [Joshen] We only need this data if the org has project scoped roles - const { data } = useProjectsQuery({ enabled: hasProjectScopedRoles }) + const { data } = useProjectsQuery({ enabled: isOptedIntoProjectLevelPermissions }) const allProjects = data?.projects ?? [] const memberIsUser = member.gotrue_id == profile?.gotrue_id diff --git a/apps/studio/components/interfaces/Organization/TeamSettings/MemberRow.tsx b/apps/studio/components/interfaces/Organization/TeamSettings/MemberRow.tsx index 236a3a9fac176..113a1f3c74f77 100644 --- a/apps/studio/components/interfaces/Organization/TeamSettings/MemberRow.tsx +++ b/apps/studio/components/interfaces/Organization/TeamSettings/MemberRow.tsx @@ -1,11 +1,13 @@ import { ArrowRight, Check, Minus, User, X } from 'lucide-react' import Link from 'next/link' +import { useParams } from 'common' import PartnerIcon from 'components/ui/PartnerIcon' import { ProfileImage } from 'components/ui/ProfileImage' import { useOrganizationRolesV2Query } from 'data/organization-members/organization-roles-query' import { OrganizationMember } from 'data/organizations/organization-members-query' import { useProjectsQuery } from 'data/projects/projects-query' +import { useHasAccessToProjectLevelPermissions } from 'data/subscriptions/org-subscription-query' import { useSelectedOrganizationQuery } from 'hooks/misc/useSelectedOrganization' import { getGitHubProfileImgUrl } from 'lib/github' import { useProfile } from 'lib/profile' @@ -32,8 +34,10 @@ const MEMBER_ORIGIN_TO_MANAGED_BY = { } as const export const MemberRow = ({ member }: MemberRowProps) => { + const { slug } = useParams() const { profile } = useProfile() const { data: selectedOrganization } = useSelectedOrganizationQuery() + const isOptedIntoProjectLevelPermissions = useHasAccessToProjectLevelPermissions(slug as string) const { data: roles, isLoading: isLoadingRoles } = useOrganizationRolesV2Query({ slug: selectedOrganization?.slug, @@ -41,7 +45,7 @@ export const MemberRow = ({ member }: MemberRowProps) => { const hasProjectScopedRoles = (roles?.project_scoped_roles ?? []).length > 0 // [Joshen] We only need this data if the org has project scoped roles - const { data } = useProjectsQuery({ enabled: hasProjectScopedRoles }) + const { data } = useProjectsQuery({ enabled: isOptedIntoProjectLevelPermissions }) const projects = data?.projects ?? [] const orgProjects = projects?.filter((p) => p.organization_id === selectedOrganization?.id) diff --git a/apps/studio/components/interfaces/Organization/TeamSettings/UpdateRolesPanel/UpdateRolesConfirmationModal.tsx b/apps/studio/components/interfaces/Organization/TeamSettings/UpdateRolesPanel/UpdateRolesConfirmationModal.tsx index e8fc5a12b5b06..7e87156097cfd 100644 --- a/apps/studio/components/interfaces/Organization/TeamSettings/UpdateRolesPanel/UpdateRolesConfirmationModal.tsx +++ b/apps/studio/components/interfaces/Organization/TeamSettings/UpdateRolesPanel/UpdateRolesConfirmationModal.tsx @@ -14,6 +14,7 @@ import { import { organizationKeys as organizationKeysV1 } from 'data/organizations/keys' import { OrganizationMember } from 'data/organizations/organization-members-query' import { useProjectsQuery } from 'data/projects/projects-query' +import { useHasAccessToProjectLevelPermissions } from 'data/subscriptions/org-subscription-query' import { useSelectedOrganizationQuery } from 'hooks/misc/useSelectedOrganization' import ConfirmationModal from 'ui-patterns/Dialogs/ConfirmationModal' import { @@ -39,12 +40,12 @@ export const UpdateRolesConfirmationModal = ({ const { slug } = useParams() const queryClient = useQueryClient() const { data: organization } = useSelectedOrganizationQuery() + const isOptedIntoProjectLevelPermissions = useHasAccessToProjectLevelPermissions(slug as string) const { data: allRoles } = useOrganizationRolesV2Query({ slug: organization?.slug }) - const hasProjectScopedRoles = (allRoles?.project_scoped_roles ?? []).length > 0 // [Joshen] We only need this data if the org has project scoped roles - const { data } = useProjectsQuery({ enabled: hasProjectScopedRoles }) + const { data } = useProjectsQuery({ enabled: isOptedIntoProjectLevelPermissions && visible }) const projects = data?.projects ?? [] // [Joshen] Separate saving state instead of using RQ due to several successive steps diff --git a/apps/studio/components/interfaces/Organization/TeamSettings/UpdateRolesPanel/UpdateRolesPanel.tsx b/apps/studio/components/interfaces/Organization/TeamSettings/UpdateRolesPanel/UpdateRolesPanel.tsx index 64e649556b8a1..f510517f6d1ae 100644 --- a/apps/studio/components/interfaces/Organization/TeamSettings/UpdateRolesPanel/UpdateRolesPanel.tsx +++ b/apps/studio/components/interfaces/Organization/TeamSettings/UpdateRolesPanel/UpdateRolesPanel.tsx @@ -63,12 +63,12 @@ export const UpdateRolesPanel = ({ visible, member, onClose }: UpdateRolesPanelP const isOptedIntoProjectLevelPermissions = useHasAccessToProjectLevelPermissions(slug as string) const { data: allRoles, isSuccess: isSuccessRoles } = useOrganizationRolesV2Query({ slug }) - const hasProjectScopedRoles = (allRoles?.project_scoped_roles ?? []).length > 0 - // [Joshen] We only need this data if the org has project scoped roles - const { data } = useProjectsQuery({ enabled: hasProjectScopedRoles }) + // Only need to fetch projects for organizations with access to project scoped roles + // which will be Team or Enterprise (and when the panel is visible) + // We still need to use the old projects endpoint instead of org projects due to roles depending on project ID + const { data } = useProjectsQuery({ enabled: isOptedIntoProjectLevelPermissions && visible }) const projects = data?.projects ?? [] - const { data: permissions } = usePermissionsQuery() // [Joshen] We use the org scoped roles as the source for available roles diff --git a/apps/studio/components/interfaces/TableGridEditor/TableGridEditor.tsx b/apps/studio/components/interfaces/TableGridEditor/TableGridEditor.tsx index b9c6d19ce1c8c..1b01e98e2af49 100644 --- a/apps/studio/components/interfaces/TableGridEditor/TableGridEditor.tsx +++ b/apps/studio/components/interfaces/TableGridEditor/TableGridEditor.tsx @@ -16,6 +16,7 @@ import { } from 'data/table-editor/table-editor-types' import { useAsyncCheckPermissions } from 'hooks/misc/useCheckPermissions' import { useDashboardHistory } from 'hooks/misc/useDashboardHistory' +import { useQuerySchemaState } from 'hooks/misc/useSchemaQueryState' import { useUrlState } from 'hooks/ui/useUrlState' import { useIsProtectedSchema } from 'hooks/useProtectedSchemas' import { TableEditorTableStateContextProvider } from 'state/table-editor-table' @@ -38,6 +39,7 @@ export const TableGridEditor = ({ const router = useRouter() const { ref: projectRef, id } = useParams() const { setLastVisitedTable } = useDashboardHistory() + const { selectedSchema } = useQuerySchemaState() const tabs = useTabsStateSnapshot() @@ -61,7 +63,9 @@ export const TableGridEditor = ({ const onTableCreated = useCallback( (table: { id: number }) => { - router.push(`/project/${projectRef}/editor/${table.id}`) + router.push( + `/project/${projectRef}/editor/${table.id}${!!selectedSchema ? `?schema=${selectedSchema}` : ''}` + ) }, [projectRef, router] ) diff --git a/apps/studio/csp.js b/apps/studio/csp.js index 5c94f4093e19f..1b58f5adf2f84 100644 --- a/apps/studio/csp.js +++ b/apps/studio/csp.js @@ -103,6 +103,7 @@ module.exports.getCSP = function getCSP() { GOOGLE_MAPS_API_URL, POSTHOG_URL, ...(!!NIMBUS_PROD_PROJECTS_URL ? [NIMBUS_PROD_PROJECTS_URL, NIMBUS_PROD_PROJECTS_URL_WS] : []), + CLOUDFLARE_CDN_URL, ] const SCRIPT_SRC_URLS = [ CLOUDFLARE_CDN_URL, diff --git a/apps/studio/data/tables/keys.ts b/apps/studio/data/tables/keys.ts index ae12de3699611..7eb0142fed479 100644 --- a/apps/studio/data/tables/keys.ts +++ b/apps/studio/data/tables/keys.ts @@ -3,10 +3,10 @@ export const tableKeys = { ['projects', projectRef, 'tables', schema, includeColumns].filter(Boolean), retrieve: (projectRef: string | undefined, name: string, schema: string) => ['projects', projectRef, 'tables', schema, name].filter(Boolean), - rolesAccess: (projectRef: string | undefined, schema: string, table: string) => [ + rolesAccess: (projectRef: string | undefined, schema: string) => [ 'projects', projectRef, 'roles-access', - { schema, table }, + { schema }, ], } diff --git a/apps/studio/data/tables/table-roles-access-query.ts b/apps/studio/data/tables/table-roles-access-query.ts deleted file mode 100644 index d47f2a0487732..0000000000000 --- a/apps/studio/data/tables/table-roles-access-query.ts +++ /dev/null @@ -1,70 +0,0 @@ -import { useQuery, UseQueryOptions } from '@tanstack/react-query' -import { executeSql, ExecuteSqlError } from '../sql/execute-sql-query' -import { tableKeys } from './keys' - -type TableRolesAccessArgs = { - schema: string - table: string -} - -/** - * [Joshen] Specifically just checking for anon and authenticated roles since this is - * just to verify if the table is exposed via the Supabase API - */ -export const getTableRolesAccessSql = ({ schema, table }: TableRolesAccessArgs) => { - const sql = /* SQL */ ` -SELECT grantee, privilege_type -FROM information_schema.role_table_grants -WHERE table_schema = '${schema}' - AND table_name = '${table}' - AND grantee IN ('anon', 'authenticated'); -`.trim() - - return sql -} - -export type TableRolesAccessVariables = TableRolesAccessArgs & { - projectRef?: string - connectionString?: string | null -} - -export async function getTableRolesAccess( - { schema, table, projectRef, connectionString }: TableRolesAccessVariables, - signal?: AbortSignal -) { - if (!schema) { - throw new Error('schema is required') - } - - const sql = getTableRolesAccessSql({ schema, table }) - - const { result } = (await executeSql( - { projectRef, connectionString, sql, queryKey: ['TableRolesAccess', schema] }, - signal - )) as { result: { grantee: string; privilege_type: string }[] } - - const res = [] - if (result.some((x) => x.grantee === 'anon')) res.push('anon') - if (result.some((x) => x.grantee === 'authenticated')) res.push('authenticated') - - return res -} - -export type TableRolesAccessData = Awaited> -export type TableRolesAccessError = ExecuteSqlError - -export const useTableRolesAccessQuery = ( - { projectRef, connectionString, schema, table }: TableRolesAccessVariables, - { - enabled = true, - ...options - }: UseQueryOptions = {} -) => - useQuery( - tableKeys.rolesAccess(projectRef, schema, table), - ({ signal }) => getTableRolesAccess({ projectRef, connectionString, schema, table }, signal), - { - enabled: enabled && typeof projectRef !== 'undefined' && typeof schema !== 'undefined', - ...options, - } - ) diff --git a/apps/studio/data/tables/tables-roles-access-query.ts b/apps/studio/data/tables/tables-roles-access-query.ts new file mode 100644 index 0000000000000..70bd5e0fc2a6a --- /dev/null +++ b/apps/studio/data/tables/tables-roles-access-query.ts @@ -0,0 +1,52 @@ +import { getTablesWithAnonAuthenticatedAccessSQL } from '@supabase/pg-meta/src/sql/studio/check-tables-anon-authenticated-access' +import { useQuery, UseQueryOptions } from '@tanstack/react-query' + +import { executeSql, ExecuteSqlError } from '../sql/execute-sql-query' +import { tableKeys } from './keys' + +type TablesRolesAccessArgs = { + schema: string +} + +export type TablesRolesAccessVariables = TablesRolesAccessArgs & { + projectRef?: string + connectionString?: string | null +} + +export async function getTablesWithAnonAuthenticatedAccess( + { schema, projectRef, connectionString }: TablesRolesAccessVariables, + signal?: AbortSignal +) { + if (!schema) throw new Error('schema is required') + + const sql = getTablesWithAnonAuthenticatedAccessSQL({ schema }) + + const { result } = (await executeSql( + { projectRef, connectionString, sql, queryKey: ['TablesRolesAccess', schema] }, + signal + )) as { result: { table_name: string }[] } + + return new Set(result.map((r) => r.table_name)) +} + +export type TablesRolesAccessData = Awaited> +export type TablesRolesAccessError = ExecuteSqlError + +export const useTablesRolesAccessQuery = ( + { projectRef, connectionString, schema }: TablesRolesAccessVariables, + { + enabled = true, + ...options + }: UseQueryOptions = {} +) => + useQuery({ + queryKey: tableKeys.rolesAccess(projectRef, schema), + queryFn: ({ signal }) => + getTablesWithAnonAuthenticatedAccess({ projectRef, connectionString, schema }, signal), + enabled: + enabled && + typeof projectRef !== 'undefined' && + typeof schema !== 'undefined' && + !!connectionString, + ...options, + }) diff --git a/apps/studio/lib/github.ts b/apps/studio/lib/github.ts index 454fa222ee710..b78986c52e31b 100644 --- a/apps/studio/lib/github.ts +++ b/apps/studio/lib/github.ts @@ -2,7 +2,7 @@ import { LOCAL_STORAGE_KEYS } from 'common' import { makeRandomString } from './helpers' const GITHUB_INTEGRATION_APP_NAME = - process.env.NIMBUS_PROD_PROJECTS_URL !== undefined + process.env.NEXT_PUBLIC_IS_NIMBUS !== undefined ? 'supabase-snap' : process.env.NEXT_PUBLIC_ENVIRONMENT === 'prod' ? `supabase` @@ -11,7 +11,7 @@ const GITHUB_INTEGRATION_APP_NAME = : `supabase-local-testing` const GITHUB_INTEGRATION_CLIENT_ID = - process.env.NIMBUS_PROD_PROJECTS_URL !== undefined + process.env.NEXT_PUBLIC_IS_NIMBUS !== undefined ? 'Iv23li2pAiqDGgaSrP8q' : process.env.NEXT_PUBLIC_ENVIRONMENT === 'prod' ? `Iv1.b91a6d8eaa272168` diff --git a/apps/studio/pages/new/[slug].tsx b/apps/studio/pages/new/[slug].tsx index 9d4dbad820c3d..ccfdc593341f1 100644 --- a/apps/studio/pages/new/[slug].tsx +++ b/apps/studio/pages/new/[slug].tsx @@ -10,10 +10,8 @@ import { z } from 'zod' import { PopoverSeparator } from '@ui/components/shadcn/ui/popover' import { LOCAL_STORAGE_KEYS, useFlag, useParams } from 'common' -import { - FreeProjectLimitWarning, - NotOrganizationOwnerWarning, -} from 'components/interfaces/Organization/NewProject' +import { NotOrganizationOwnerWarning } from 'components/interfaces/Organization/NewProject' +import { FreeProjectLimitWarning } from 'components/interfaces/Organization/NewProject/FreeProjectLimitWarning' import { OrgNotFound } from 'components/interfaces/Organization/OrgNotFound' import { AdvancedConfiguration } from 'components/interfaces/ProjectCreation/AdvancedConfiguration' import { @@ -972,10 +970,7 @@ const Wizard: NextPageWithLayout = () => { isAdmin && slug && ( - + ) ) : isManagedByVercel ? ( diff --git a/apps/studio/pages/project/[ref]/editor/index.tsx b/apps/studio/pages/project/[ref]/editor/index.tsx index 9d1d533a775a2..b6ffc7c364481 100644 --- a/apps/studio/pages/project/[ref]/editor/index.tsx +++ b/apps/studio/pages/project/[ref]/editor/index.tsx @@ -10,6 +10,7 @@ import TableEditorLayout from 'components/layouts/TableEditorLayout/TableEditorL import { TableEditorMenu } from 'components/layouts/TableEditorLayout/TableEditorMenu' import { NewTab } from 'components/layouts/Tabs/NewTab' import { useDashboardHistory } from 'hooks/misc/useDashboardHistory' +import { useQuerySchemaState } from 'hooks/misc/useSchemaQueryState' import { editorEntityTypes, useTabsStateSnapshot } from 'state/tabs' import type { NextPageWithLayout } from 'types' @@ -17,10 +18,13 @@ const TableEditorPage: NextPageWithLayout = () => { const router = useRouter() const { ref: projectRef } = useParams() const tabStore = useTabsStateSnapshot() + const { selectedSchema } = useQuerySchemaState() const { history, isHistoryLoaded } = useDashboardHistory() const onTableCreated = (table: { id: number }) => { - router.push(`/project/${projectRef}/editor/${table.id}`) + router.push( + `/project/${projectRef}/editor/${table.id}${!!selectedSchema ? `?schema=${selectedSchema}` : ''}` + ) } useEffect(() => { diff --git a/apps/studio/state/tabs.tsx b/apps/studio/state/tabs.tsx index f7ddd272e5dc5..7b282f2128b69 100644 --- a/apps/studio/state/tabs.tsx +++ b/apps/studio/state/tabs.tsx @@ -306,13 +306,22 @@ function createTabsState(projectRef: string) { onClearDashboardHistory: () => void }) => { const tabBeingClosed = store.tabsMap[id] - const tabsAfterClosing = Object.values(store.tabsMap).filter((tab) => tab.id !== id) - const nextTabId = !editor - ? undefined - : tabsAfterClosing.filter((tab) => { - return editorEntityTypes[editor]?.includes(tab.type) - })[0]?.id + const editorTabIds = ( + editor + ? Object.values(store.tabsMap).filter((tab) => + editorEntityTypes[editor]?.includes(tab.type) + ) + : [] + ).map((tab) => tab.id) + const tabIndexBeingClosed = editorTabIds.indexOf(id) + const isLastTabBeingClosed = tabIndexBeingClosed === editorTabIds.length - 1 + const nextTabId = + editorTabIds.length === 1 + ? undefined + : isLastTabBeingClosed + ? editorTabIds[tabIndexBeingClosed - 1] + : editorTabIds[tabIndexBeingClosed + 1] const { [id]: value, ...otherTabs } = store.tabsMap store.tabsMap = otherTabs diff --git a/packages/pg-meta/src/sql/studio/check-tables-anon-authenticated-access.ts b/packages/pg-meta/src/sql/studio/check-tables-anon-authenticated-access.ts new file mode 100644 index 0000000000000..ceaf86c7fa5ad --- /dev/null +++ b/packages/pg-meta/src/sql/studio/check-tables-anon-authenticated-access.ts @@ -0,0 +1,20 @@ +/** + * Given a schema name, list all the tables in that schema which have access + * granted to either the "anon" or "authenticated" role + */ +export const getTablesWithAnonAuthenticatedAccessSQL = ({ schema }: { schema: string }) => + /* SQL */ ` +SELECT c.relname AS table_name +FROM pg_catalog.pg_class AS c +JOIN pg_catalog.pg_namespace AS n ON n.oid = c.relnamespace +WHERE n.nspname = '${schema}' + AND c.relkind IN ('r','p') -- table, partitioned table + AND EXISTS ( + SELECT 1 + FROM pg_catalog.aclexplode(COALESCE(c.relacl, '{}'::aclitem[])) AS a + JOIN pg_catalog.pg_roles r ON r.oid = a.grantee + WHERE r.rolname IN ('anon','authenticated') + ) +; +`.trim() + diff --git a/packages/ui/src/components/shadcn/ui/sidebar.tsx b/packages/ui/src/components/shadcn/ui/sidebar.tsx index a8e1bc59e4299..a731edcfec07d 100644 --- a/packages/ui/src/components/shadcn/ui/sidebar.tsx +++ b/packages/ui/src/components/shadcn/ui/sidebar.tsx @@ -219,8 +219,8 @@ const Sidebar = React.forwardRef< {/* This is what handles the sidebar gap on desktop */}