adds support for loading views and foreign tables

This commit is contained in:
Alaister Young authored and Joshen Lim committed 2023-03-23 13:56:21 +08:00
1 parent 80ff7827dc
commit fc7d2882d0
17 files changed
+407 -98

No files matched your search

@@ -1,4 +1,4 @@
import { FC, useRef, useEffect, useState } from 'react'
import { useRef, useEffect, useState } from 'react'
import { observer } from 'mobx-react-lite'
import { useRouter } from 'next/router'
import { find, isUndefined, noop } from 'lodash'
@@ -1,13 +1,18 @@
import { PropsWithChildren, useEffect } from 'react'
import { isUndefined, noop } from 'lodash'
import { PropsWithChildren, useEffect, useState } from 'react'
import { useRouter } from 'next/router'
import { noop } from 'lodash'
import { observer } from 'mobx-react-lite'
import { PermissionAction } from '@supabase/shared-types/out/constants'
import { checkPermissions, useParams, useStore } from 'hooks'
import { Entity } from 'data/entity-types/entity-types-infinite-query'
import { Entity } from 'data/entity-types/entity-type-query'
import ProjectLayout from '../ProjectLayout/ProjectLayout'
import TableEditorMenu from './TableEditorMenu'
import NoPermission from 'components/ui/NoPermission'
import useEntityType from 'hooks/misc/useEntityType'
import Connecting from 'components/ui/Loading/Loading'
import useTableRowsPrefetch from 'hooks/misc/useTableRowsPrefetch'
import useLatest from 'hooks/misc/useLatest'
export interface TableEditorLayoutProps {
selectedSchema?: string
@@ -28,7 +33,9 @@ const TableEditorLayout = ({
children,
}: PropsWithChildren<TableEditorLayoutProps>) => {
const { vault, meta, ui } = useStore()
const { id, type } = useParams()
const router = useRouter()
const { id: _id } = useParams()
const id = _id ? Number(_id) : undefined
const canReadTables = checkPermissions(PermissionAction.TENANT_SQL_ADMIN_READ, 'tables')
@@ -42,22 +49,70 @@ const TableEditorLayout = ({
meta.policies.load()
meta.publications.load()
meta.extensions.load()
// [Joshen] pg-meta doesn't support loading views nor foreign tables by a specific ID yet
// Separately, Alaister and I chatted that perhaps we leverage on pg-catalog's pg-class
// table directly, so we can fetch the schema of tables/views/foreign-tables in one call
// rather than trying to discern if the ID is a view, or foreign table (refer to below)
meta.views.load()
meta.foreignTables.load()
}
}, [ui.selectedProject?.ref])
const [loadedIds, setLoadedIds] = useState<Set<number>>(() => new Set())
const isLoaded = id !== undefined && loadedIds.has(id)
const entity = useEntityType(id, function onNotFound(id) {
setLoadedIds((loadedIds) => {
const newLoadedIds = new Set(loadedIds)
newLoadedIds.add(id)
return newLoadedIds
})
})
const prefetch = useLatest(useTableRowsPrefetch())
useEffect(() => {
if (ui.selectedProject?.ref && id) {
meta.tables.loadById(Number(id))
meta.views.loadById(Number(id))
let mounted = true
function loadTable() {
if (entity?.type) {
switch (entity.type) {
case 'materialized_view':
case 'view':
return meta.views.loadById(entity.id)
case 'foreign_table':
return meta.foreignTables.loadById(entity.id)
default:
return meta.tables.loadById(entity.id)
}
}
}
}, [ui.selectedProject?.ref, id])
loadTable()
?.then(async (entity: any) => {
await prefetch.current(entity)
return entity
})
.then((entity: any) => {
if (mounted) {
setLoadedIds((loadedIds) => {
const newLoadedIds = new Set(loadedIds)
newLoadedIds.add(entity.id ?? entity?.id)
return newLoadedIds
})
}
})
.catch(() => {
if (mounted && entity?.id) {
setLoadedIds((loadedIds) => {
const newLoadedIds = new Set(loadedIds)
newLoadedIds.add(entity.id)
return newLoadedIds
})
}
})
return () => {
mounted = false
}
}, [entity])
useEffect(() => {
if (isVaultEnabled) {
@@ -75,7 +130,6 @@ const TableEditorLayout = ({
return (
<ProjectLayout
isLoading={isUndefined(selectedSchema)}
product="Table editor"
productMenu={
<TableEditorMenu
@@ -88,7 +142,7 @@ const TableEditorLayout = ({
/>
}
>
{children}
{router.isReady && id !== undefined ? isLoaded ? children : <Connecting /> : children}
</ProjectLayout>
)
}
@@ -24,7 +24,8 @@ import type { PostgresSchema } from '@supabase/postgres-meta'
import { checkPermissions, useStore, useParams } from 'hooks'
import ProductMenuItem from 'components/ui/ProductMenu/ProductMenuItem'
import { Entity, useEntityTypesQuery } from 'data/entity-types/entity-types-infinite-query'
import { useEntityTypesQuery } from 'data/entity-types/entity-types-infinite-query'
import { Entity } from 'data/entity-types/entity-type-query'
import { useProjectContext } from '../ProjectLayout/ProjectContext'
import InfiniteList from 'components/ui/InfiniteList'
@@ -1,4 +1,5 @@
import { UseQueryOptions } from '@tanstack/react-query'
import { useCallback } from 'react'
import { ExecuteSqlData, useExecuteSqlPrefetch, useExecuteSqlQuery } from '../sql/execute-sql-query'
type getForeignKeyConstraintsVariables = {
@@ -107,15 +108,17 @@ export const useForeignKeyConstraintsQuery = <
)
}
export const useForeignKeyConstraintsPrefetch = ({
projectRef,
connectionString,
schema,
}: ForeignKeyConstraintsVariables) => {
return useExecuteSqlPrefetch({
projectRef,
connectionString,
sql: getForeignKeyConstraintsQuery({ schema }),
queryKey: ['foreign-key-constraints'],
})
export const useForeignKeyConstraintsPrefetch = () => {
const prefetch = useExecuteSqlPrefetch()
return useCallback(
({ projectRef, connectionString, schema }: ForeignKeyConstraintsVariables) =>
prefetch({
projectRef,
connectionString,
sql: getForeignKeyConstraintsQuery({ schema }),
queryKey: ['foreign-key-constraints'],
}),
[prefetch]
)
}
@@ -0,0 +1,109 @@
import { UseQueryOptions } from '@tanstack/react-query'
import { useCallback } from 'react'
import { ExecuteSqlData, useExecuteSqlPrefetch, useExecuteSqlQuery } from '../sql/execute-sql-query'
type EntityTypeArgs = {
id?: number
}
export const entityTypeSqlQuery = ({ id }: EntityTypeArgs) => {
const sql = /* SQL */ `
select
c.oid::int8 as "id",
nc.nspname as "schema",
c.relname as "name",
case c.relkind
when 'r' then 'table'
when 'v' then 'view'
when 'm' then 'materialized_view'
when 'f' then 'foreign_table'
when 'p' then 'partitioned_table'
end as "type",
obj_description(c.oid) as "comment",
count(*) over() as "count"
from
pg_namespace nc
join pg_class c on nc.oid = c.relnamespace
where
c.relkind in ('r', 'v', 'm', 'f', 'p')
and not pg_is_other_temp_schema(nc.oid)
and (
pg_has_role(c.relowner, 'USAGE')
or has_table_privilege(
c.oid,
'SELECT, INSERT, UPDATE, DELETE, TRUNCATE, REFERENCES, TRIGGER'
)
or has_any_column_privilege(c.oid, 'SELECT, INSERT, UPDATE, REFERENCES')
)
and c.oid = '${id}'
limit 1;
`
return sql
}
export type Entity = {
id: number
schema: string
name: string
type: 'table' | 'view' | 'materialized_view' | 'foreign_table' | 'partitioned_table'
comment: string | null
}
export type EntityType = Entity | null
export type EntityTypeVariables = EntityTypeArgs & {
projectRef?: string
connectionString?: string
}
export type EntityTypeData = EntityType
export type EntityTypeError = unknown
export const useEntityTypeQuery = <TData extends EntityTypeData = EntityTypeData>(
{ projectRef, connectionString, id, ...args }: EntityTypeVariables,
{ enabled, ...options }: UseQueryOptions<ExecuteSqlData, EntityTypeError, TData> = {}
) =>
useExecuteSqlQuery(
{
projectRef,
connectionString,
sql: entityTypeSqlQuery({ id, ...args }),
queryKey: ['entity-type', id],
},
{
select(data) {
return (data.result[0] ?? null) as TData
},
enabled:
enabled && typeof projectRef !== 'undefined' && typeof id !== 'undefined' && !isNaN(id),
...options,
}
)
/**
* useEntityTypePrefetch is used for prefetching table rows. For example, starting a query loading before a page is navigated to.
*
* @example
* const prefetch = useEntityTypePrefetch()
*
* return (
* <Link onMouseEnter={() => prefetch({ ...args })}>
* Start loading on hover
* </Link>
* )
*/
export const useEntityTypePrefetch = () => {
const prefetch = useExecuteSqlPrefetch()
return useCallback(
({ projectRef, connectionString, id, ...args }: EntityTypeVariables) =>
prefetch({
projectRef,
connectionString,
sql: entityTypeSqlQuery({ id, ...args }),
queryKey: ['entity-type', id],
}),
[prefetch]
)
}
@@ -1,5 +1,6 @@
import { useInfiniteQuery, UseInfiniteQueryOptions } from '@tanstack/react-query'
import { executeSql, ExecuteSqlVariables } from 'data/sql/execute-sql-query'
import { Entity } from './entity-type-query'
import { entityTypeKeys } from './keys'
export type EntityTypesVariables = {
@@ -9,14 +10,6 @@ export type EntityTypesVariables = {
page?: number
} & Pick<ExecuteSqlVariables, 'connectionString'>
export type Entity = {
id: number
schema: string
name: string
type: string
comment: string | null
}
export type EntityTypesResponse = {
data: {
entities: Entity[]
+14 -7
View File
@@ -1,4 +1,5 @@
import { UseQueryOptions } from '@tanstack/react-query'
import { useCallback } from 'react'
import { ExecuteSqlData, useExecuteSqlPrefetch, useExecuteSqlQuery } from '../sql/execute-sql-query'
export const getFDWsSql = () => {
@@ -88,11 +89,17 @@ export const useFDWsQuery = <TData extends FDWsData = FDWsData>(
options
)
export const useFDWsPrefetch = ({ projectRef, connectionString }: FDWsVariables) => {
return useExecuteSqlPrefetch({
projectRef,
connectionString,
sql: getFDWsSql(),
queryKey: ['fdws'],
})
export const useFDWsPrefetch = () => {
const prefetch = useExecuteSqlPrefetch()
return useCallback(
({ projectRef, connectionString }: FDWsVariables) =>
prefetch({
projectRef,
connectionString,
sql: getFDWsSql(),
queryKey: ['fdws'],
}),
[prefetch]
)
}
+44 -14
View File
@@ -1,4 +1,10 @@
import { QueryKey, useQuery, useQueryClient, UseQueryOptions } from '@tanstack/react-query'
import {
QueryClient,
QueryKey,
useQuery,
useQueryClient,
UseQueryOptions,
} from '@tanstack/react-query'
import md5 from 'blueimp-md5'
import { post } from 'lib/common/fetch'
import { API_URL } from 'lib/constants'
@@ -55,19 +61,43 @@ export const useExecuteSqlQuery = <TData = ExecuteSqlData>(
{ enabled: enabled && typeof projectRef !== 'undefined', ...options }
)
export const useExecuteSqlPrefetch = ({
projectRef,
connectionString,
sql,
queryKey,
}: ExecuteSqlVariables) => {
export const prefetchExecuteSql = (
client: QueryClient,
{ projectRef, connectionString, sql, queryKey }: ExecuteSqlVariables
) => {
return client.prefetchQuery(sqlKeys.query(projectRef, queryKey ?? [md5(sql)]), ({ signal }) =>
executeSql({ projectRef, connectionString, sql }, signal)
)
}
/**
* useExecuteSqlPrefetch is used for prefetching a SQL query. For example, starting a query loading before a page is navigated to.
*
* @example
* const prefetch = useExecuteSqlPrefetch()
*
* return (
* <Link onMouseEnter={() => prefetch({ ...args })}>
* Start loading on hover
* </Link>
* )
*/
export const useExecuteSqlPrefetch = () => {
const client = useQueryClient()
return useCallback(() => {
if (projectRef) {
client.prefetchQuery(sqlKeys.query(projectRef, queryKey ?? [md5(sql)]), ({ signal }) =>
executeSql({ projectRef, connectionString, sql }, signal)
)
}
}, [projectRef])
return useCallback(
({ projectRef, connectionString, sql, queryKey }: ExecuteSqlVariables) => {
if (projectRef) {
return prefetchExecuteSql(client, {
projectRef,
connectionString,
sql,
queryKey,
})
}
return Promise.resolve()
},
[client]
)
}
@@ -1,5 +1,6 @@
import { QueryKey, UseQueryOptions } from '@tanstack/react-query'
import { Filter, Query, SupaTable } from 'components/grid'
import { useCallback } from 'react'
import { ExecuteSqlData, useExecuteSqlPrefetch, useExecuteSqlQuery } from '../sql/execute-sql-query'
import { formatFilterValue } from './utils'
@@ -70,28 +71,28 @@ export const useTableRowsCountQuery = <TData extends TableRowsCountData = TableR
* useTableRowsCountPrefetch is used for prefetching the table rows count. For example, starting a query loading before a page is navigated to.
*
* @example
* const prefetch = useTableRowsCountPrefetch({ ...args })
* const prefetch = useTableRowsCountPrefetch()
*
* return (
* <Link onMouseEnter={() => prefetch()}>
* <Link onMouseEnter={() => prefetch({ ...args })}>
* Start loading on hover
* </Link>
* )
*/
export const useTableRowsCountPrefetch = ({
projectRef,
connectionString,
queryKey,
table,
...args
}: TableRowsCountVariables) => {
return useExecuteSqlPrefetch({
projectRef,
connectionString,
sql: getTableRowsCountSqlQuery({ table, ...args }),
queryKey: [
...(queryKey ?? []),
{ table: { name: table?.name, schema: table?.schema }, ...args },
],
})
export const useTableRowsCountPrefetch = () => {
const prefetch = useExecuteSqlPrefetch()
return useCallback(
({ projectRef, connectionString, queryKey, table, ...args }: TableRowsCountVariables) =>
prefetch({
projectRef,
connectionString,
sql: getTableRowsCountSqlQuery({ table, ...args }),
queryKey: [
...(queryKey ?? []),
{ table: { name: table?.name, schema: table?.schema }, ...args },
],
}),
[prefetch]
)
}
+17 -16
View File
@@ -1,5 +1,6 @@
import { QueryKey, UseQueryOptions } from '@tanstack/react-query'
import { Filter, Query, Sort, SupaRow, SupaTable } from 'components/grid'
import { useCallback } from 'react'
import { ExecuteSqlData, useExecuteSqlPrefetch, useExecuteSqlQuery } from '../sql/execute-sql-query'
import { getPagination } from '../utils/pagination'
import { formatFilterValue } from './utils'
@@ -109,20 +110,20 @@ export const useTableRowsQuery = <TData extends TableRowsData = TableRowsData>(
* </Link>
* )
*/
export const useTableRowsPrefetch = ({
projectRef,
connectionString,
queryKey,
table,
...args
}: TableRowsVariables) => {
return useExecuteSqlPrefetch({
projectRef,
connectionString,
sql: getTableRowsSqlQuery({ table, ...args }),
queryKey: [
...(queryKey ?? []),
{ table: { name: table?.name, schema: table?.schema }, ...args },
],
})
export const useTableRowsPrefetch = () => {
const prefetch = useExecuteSqlPrefetch()
return useCallback(
({ projectRef, connectionString, queryKey, table, ...args }: TableRowsVariables) =>
prefetch({
projectRef,
connectionString,
sql: getTableRowsSqlQuery({ table, ...args }),
queryKey: [
...(queryKey ?? []),
{ table: { name: table?.name, schema: table?.schema }, ...args },
],
}),
[prefetch]
)
}
+43
View File
@@ -0,0 +1,43 @@
import { useQueryClient } from '@tanstack/react-query'
import { useProjectContext } from 'components/layouts/ProjectLayout/ProjectContext'
import { Entity, useEntityTypeQuery } from 'data/entity-types/entity-type-query'
import { entityTypeKeys } from 'data/entity-types/keys'
import { useMemo } from 'react'
/**
* Tries to load an entity type from the cached list of entity types, or fetches it from the server.
*/
function useEntityType(id?: number, onNotFound?: (id: number) => void) {
const queryClient = useQueryClient()
const { project } = useProjectContext()
const existingEntity = useMemo(
() =>
(
queryClient.getQueryCache().find(entityTypeKeys.list(project?.ref))?.state.data as any
)?.pages
?.flatMap((page: any) => page.data.entities)
?.find((entity: any) => entity.id === id) as Entity | undefined,
[project?.ref, id]
)
const { data: entity } = useEntityTypeQuery(
{
projectRef: project?.ref,
connectionString: project?.connectionString,
id,
},
{
enabled: !existingEntity,
onSettled(data) {
if (!data && id !== undefined) {
onNotFound?.(id)
}
},
}
)
return existingEntity ?? entity
}
export default useEntityType
+9
View File
@@ -0,0 +1,9 @@
import { useRef } from 'react'
const useLatest = <T>(value: T): { readonly current: T } => {
const ref = useRef(value)
ref.current = value
return ref
}
export default useLatest
+37
View File
@@ -0,0 +1,37 @@
import { SupaTable } from 'components/grid'
import { formatFilterURLParams, formatSortURLParams } from 'components/grid/SupabaseGrid.utils'
import { useProjectContext } from 'components/layouts/ProjectLayout/ProjectContext'
import { useTableRowsPrefetch as _useTableRowsPrefetch } from 'data/table-rows/table-rows-query'
import { useUrlState } from 'hooks'
import { useCallback } from 'react'
/**
* useTableRowsPrefetch is a wrapper around the base useTableRowsPrefetch that prefills the sort and filter params
*/
function useTableRowsPrefetch() {
const [{ sort, filter }] = useUrlState({
arrayKeys: ['sort', 'filter'],
})
const sorts = formatSortURLParams(sort as string[])
const filters = formatFilterURLParams(filter as string[])
const { project } = useProjectContext()
const prefetch = _useTableRowsPrefetch()
return useCallback(
(table: SupaTable) =>
prefetch({
queryKey: [table.schema, table.name],
projectRef: project?.ref,
connectionString: project?.connectionString,
table,
sorts,
filters,
page: 1,
limit: 100,
}),
[project, sorts, filters]
)
}
export default useTableRowsPrefetch
+1 -1
View File
@@ -7,7 +7,7 @@ import { Alert, Button, Checkbox, IconExternalLink, Modal } from 'ui'
import type { PostgresTable, PostgresColumn } from '@supabase/postgres-meta'
import { useStore, withAuth, useUrlState, useParams } from 'hooks'
import { Entity } from 'data/entity-types/entity-types-infinite-query'
import { Entity } from 'data/entity-types/entity-type-query'
import { Dictionary } from 'components/grid'
import { TableEditorLayout } from 'components/layouts'
import { TableGridEditor } from 'components/interfaces'
+1 -1
View File
@@ -6,7 +6,7 @@ import type { PostgresTable } from '@supabase/postgres-meta'
import { NextPageWithLayout } from 'types'
import { useStore, withAuth } from 'hooks'
import { Entity } from 'data/entity-types/entity-types-infinite-query'
import { Entity } from 'data/entity-types/entity-type-query'
import { TableEditorLayout } from 'components/layouts'
import { EmptyState, SidePanelEditor } from 'components/interfaces/TableGridEditor'
import ConfirmationModal from 'components/ui/ConfirmationModal'
+22 -1
View File
@@ -1,7 +1,13 @@
import type { PostgresTable } from '@supabase/postgres-meta'
import PostgresMetaInterface from '../common/PostgresMetaInterface'
import { get } from 'lib/common/fetch'
import { ResponseError } from 'types'
import PostgresMetaInterface, { IPostgresMetaInterface } from '../common/PostgresMetaInterface'
import { IRootStore } from '../RootStore'
export interface IForeignTableStore extends IPostgresMetaInterface<Partial<PostgresTable>> {
loadById: (id: number | string) => Promise<Partial<PostgresTable> | { error: ResponseError }>
}
export default class ForeignTableStore extends PostgresMetaInterface<Partial<PostgresTable>> {
constructor(
rootStore: IRootStore,
@@ -18,4 +24,19 @@ export default class ForeignTableStore extends PostgresMetaInterface<Partial<Pos
async loadBySchema(schema: string) {
return []
}
async loadById(id: number | string) {
try {
const url = this.url.includes('?') ? `${this.url}&id=${id}` : `${this.url}?id=${id}`
const response = await get(url, { headers: this.headers })
if (response.error) throw response.error
const data = response as Partial<PostgresTable>
// @ts-ignore
this.data[id] = data
return data
} catch (error: any) {
return { error }
}
}
}
+2 -2
View File
@@ -42,7 +42,7 @@ import FunctionsStore from './FunctionsStore'
import HooksStore from './HooksStore'
import ExtensionsStore from './ExtensionsStore'
import TypesStore from './TypesStore'
import ForeignTableStore from './ForeignTableStore'
import ForeignTableStore, { IForeignTableStore } from './ForeignTableStore'
import ViewStore, { IViewStore } from './ViewStore'
import { FOREIGN_KEY_DELETION_ACTION } from 'data/database/database-query-constants'
@@ -57,7 +57,7 @@ export interface IMetaStore {
columns: IPostgresMetaInterface<PostgresColumn>
schemas: IPostgresMetaInterface<PostgresSchema>
views: IViewStore
foreignTables: IPostgresMetaInterface<Partial<PostgresTable>>
foreignTables: IForeignTableStore
hooks: IPostgresMetaInterface<any>
roles: IRolesStore