mirror of
https://github.com/supabase/supabase.git
synced 2026-10-05 09:25:06 +03:00
chore: Migrate ViewsStore (#19975)
* Move useEncryptedColumns hook and use it in the TableGridEditor. * Convert the viewsQuery to use data/fetchers. It also handles invalidation of schemas. * Invalidate the view when deleting a column or a table. * Use the new query function in VaultStore. * Remove the viewStore. * Fix two more edge cases. Add comments for it.
This commit is contained in:
1 parent
bf4421aca8
commit
c867205b20
9 files changed
+85
-127
No files matched your search
@@ -14,6 +14,7 @@ import { useTableRowDeleteMutation } from 'data/table-rows/table-row-delete-muta
|
||||
import { useTableRowTruncateMutation } from 'data/table-rows/table-row-truncate-mutation'
|
||||
import { tableKeys } from 'data/tables/keys'
|
||||
import { useGetTables } from 'data/tables/tables-query'
|
||||
import { viewKeys } from 'data/views/keys'
|
||||
import { useStore, useUrlState } from 'hooks'
|
||||
import { TableLike } from 'hooks/misc/useTable'
|
||||
import { noop } from 'lib/void'
|
||||
@@ -151,9 +152,19 @@ const DeleteConfirmationDialogs = ({
|
||||
selectedTable!.name,
|
||||
])
|
||||
),
|
||||
// refetch all entities in the sidebar because deleting a column may regenerate a view (and change its id)
|
||||
queryClient.invalidateQueries(entityTypeKeys.list(projectRef)),
|
||||
// invalidate all views from this schema, not sure if this is needed since you can't actually delete a column
|
||||
// which has a view dependent on it
|
||||
snap.selectedSchemaName
|
||||
? queryClient.invalidateQueries(
|
||||
viewKeys.listBySchema(projectRef, snap.selectedSchemaName)
|
||||
)
|
||||
: null,
|
||||
// invalidate the view if there's a view with this id, not sure if this is needed because you can't delete a
|
||||
// column from a view
|
||||
queryClient.invalidateQueries(viewKeys.view(projectRef, selectedTable?.id)),
|
||||
])
|
||||
|
||||
if (snap.selectedSchemaName) await meta.views.loadBySchema(snap.selectedSchemaName)
|
||||
} catch (error: any) {
|
||||
ui.setNotification({
|
||||
category: 'error',
|
||||
@@ -176,7 +187,17 @@ const DeleteConfirmationDialogs = ({
|
||||
|
||||
const tables = await getTables(snap.selectedSchemaName)
|
||||
|
||||
await queryClient.invalidateQueries(entityTypeKeys.list(projectRef))
|
||||
await Promise.all([
|
||||
queryClient.invalidateQueries(entityTypeKeys.list(projectRef)),
|
||||
// invalidate all views from this schema
|
||||
snap.selectedSchemaName
|
||||
? queryClient.invalidateQueries(
|
||||
viewKeys.listBySchema(projectRef, snap.selectedSchemaName)
|
||||
)
|
||||
: null,
|
||||
// invalidate the view if there's a view with this id
|
||||
queryClient.invalidateQueries(viewKeys.view(projectRef, selectedTableToDelete?.id)),
|
||||
])
|
||||
|
||||
onAfterDeleteTable(tables)
|
||||
|
||||
@@ -184,7 +205,6 @@ const DeleteConfirmationDialogs = ({
|
||||
category: 'success',
|
||||
message: `Successfully deleted table "${selectedTableToDelete.name}"`,
|
||||
})
|
||||
if (snap.selectedSchemaName) await meta.views.loadBySchema(snap.selectedSchemaName)
|
||||
} catch (error: any) {
|
||||
ui.setNotification({
|
||||
error,
|
||||
|
||||
+1
-1
@@ -15,7 +15,7 @@ import { useTableRowsQuery } from 'data/table-rows/table-rows-query'
|
||||
import { useTableQuery } from 'data/tables/table-query'
|
||||
import { useRoleImpersonationStateSnapshot } from 'state/role-impersonation-state'
|
||||
import ActionBar from '../../ActionBar'
|
||||
import { useEncryptedColumns } from './ForeignRowSelector.utils'
|
||||
import { useEncryptedColumns } from '../../SidePanelEditor.utils'
|
||||
import Pagination from './Pagination'
|
||||
import SelectorGrid from './SelectorGrid'
|
||||
|
||||
|
||||
File renamed without changes.
@@ -5,10 +5,8 @@ import { useParams } from 'common'
|
||||
import { find, isUndefined } from 'lodash'
|
||||
import { observer } from 'mobx-react-lite'
|
||||
import { useRouter } from 'next/router'
|
||||
import { useEffect, useState } from 'react'
|
||||
|
||||
import { parseSupaTable, SupabaseGrid, SupaTable } from 'components/grid'
|
||||
import { Dictionary } from 'types'
|
||||
import { ERROR_PRIMARY_KEY_NOTFOUND } from 'components/grid/constants'
|
||||
import { useProjectContext } from 'components/layouts/ProjectLayout/ProjectContext'
|
||||
import Connecting from 'components/ui/Loading/Loading'
|
||||
@@ -28,11 +26,12 @@ import { EXCLUDED_SCHEMAS } from 'lib/constants/schemas'
|
||||
import { EMPTY_ARR } from 'lib/void'
|
||||
import { useGetImpersonatedRole } from 'state/role-impersonation-state'
|
||||
import { useTableEditorStateSnapshot } from 'state/table-editor'
|
||||
import { SchemaView } from 'types'
|
||||
import { Dictionary, SchemaView } from 'types'
|
||||
import { RoleImpersonationPopover } from '../RoleImpersonationSelector'
|
||||
import GridHeaderActions from './GridHeaderActions'
|
||||
import NotFoundState from './NotFoundState'
|
||||
import SidePanelEditor from './SidePanelEditor'
|
||||
import { useEncryptedColumns } from './SidePanelEditor/SidePanelEditor.utils'
|
||||
import TableDefinition from './TableDefinition'
|
||||
|
||||
export interface TableGridEditorProps {
|
||||
@@ -49,7 +48,7 @@ const TableGridEditor = ({
|
||||
selectedTable,
|
||||
}: TableGridEditorProps) => {
|
||||
const router = useRouter()
|
||||
const { meta, ui, vault } = useStore()
|
||||
const { meta, ui } = useStore()
|
||||
const { ref: projectRef, id } = useParams()
|
||||
|
||||
const { project } = useProjectContext()
|
||||
@@ -57,8 +56,6 @@ const TableGridEditor = ({
|
||||
|
||||
const getImpersonatedRole = useGetImpersonatedRole()
|
||||
|
||||
const [encryptedColumns, setEncryptedColumns] = useState([])
|
||||
|
||||
const [{ view: selectedView = 'data' }, setUrlState] = useUrlState()
|
||||
const setSelectedView = (view: string) => {
|
||||
if (view === 'data') {
|
||||
@@ -72,10 +69,10 @@ const TableGridEditor = ({
|
||||
const canEditColumns = useCheckPermissions(PermissionAction.TENANT_SQL_ADMIN_WRITE, 'columns')
|
||||
const isReadOnly = !canEditTables && !canEditColumns
|
||||
|
||||
const getEncryptedColumns = async (table: any) => {
|
||||
const columns = await vault.listEncryptedColumns(table.schema, table.name)
|
||||
setEncryptedColumns(columns)
|
||||
}
|
||||
const encryptedColumns = useEncryptedColumns({
|
||||
schemaName: selectedTable?.schema,
|
||||
tableName: selectedTable?.name,
|
||||
})
|
||||
|
||||
const queryClient = useQueryClient()
|
||||
const { mutate: mutateUpdateTableRow } = useTableRowUpdateMutation({
|
||||
@@ -143,12 +140,6 @@ const TableGridEditor = ({
|
||||
})
|
||||
const foreignKeyMeta = data || []
|
||||
|
||||
useEffect(() => {
|
||||
if (selectedTable !== undefined && selectedTable.id !== undefined) {
|
||||
getEncryptedColumns(selectedTable)
|
||||
}
|
||||
}, [selectedTable?.id])
|
||||
|
||||
const entityType = useEntityType(selectedTable?.id)
|
||||
const columnsRef = useLatest(selectedTable?.columns ?? EMPTY_ARR)
|
||||
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
export const viewKeys = {
|
||||
list: (projectRef: string | undefined) => ['projects', projectRef, 'views'] as const,
|
||||
listBySchema: (projectRef: string | undefined, schema: string) =>
|
||||
[...viewKeys.list(projectRef), schema] as const,
|
||||
view: (projectRef: string | undefined, id: number | undefined) =>
|
||||
['projects', projectRef, 'views', id] as const,
|
||||
[...viewKeys.list(projectRef), id] as const,
|
||||
}
|
||||
@@ -1,49 +1,66 @@
|
||||
import { PostgresView } from '@supabase/postgres-meta'
|
||||
import { useQuery, UseQueryOptions } from '@tanstack/react-query'
|
||||
import { get } from 'lib/common/fetch'
|
||||
import { API_URL } from 'lib/constants'
|
||||
import { UseQueryOptions, useQuery, useQueryClient } from '@tanstack/react-query'
|
||||
import { useCallback } from 'react'
|
||||
|
||||
import { get } from 'data/fetchers'
|
||||
import { ResponseError } from 'types'
|
||||
import { viewKeys } from './keys'
|
||||
import { View } from './view-query'
|
||||
|
||||
export type ViewsVariables = {
|
||||
projectRef?: string
|
||||
connectionString?: string
|
||||
schema?: string
|
||||
}
|
||||
|
||||
export type ViewsResponse = View[] | { error?: any }
|
||||
|
||||
export async function getViews(
|
||||
{ projectRef, connectionString }: ViewsVariables,
|
||||
{ projectRef, connectionString, schema }: ViewsVariables,
|
||||
signal?: AbortSignal
|
||||
) {
|
||||
if (!projectRef) {
|
||||
throw new Error('projectRef is required')
|
||||
}
|
||||
if (!projectRef) throw new Error('projectRef is required')
|
||||
|
||||
let headers = new Headers()
|
||||
if (connectionString) headers.set('x-connection-encrypted', connectionString)
|
||||
|
||||
const response = (await get(`${API_URL}/pg-meta/${projectRef}/views`, {
|
||||
headers: Object.fromEntries(headers),
|
||||
const { data, error } = await get('/platform/pg-meta/{ref}/views', {
|
||||
params: {
|
||||
header: { 'x-connection-encrypted': connectionString! },
|
||||
path: { ref: projectRef },
|
||||
query: {
|
||||
included_schemas: schema || '',
|
||||
} as any,
|
||||
},
|
||||
headers,
|
||||
signal,
|
||||
})) as ViewsResponse
|
||||
})
|
||||
|
||||
if (!Array.isArray(response) && response.error) {
|
||||
throw response.error
|
||||
}
|
||||
|
||||
return response as PostgresView[]
|
||||
if (error) throw error
|
||||
return data
|
||||
}
|
||||
|
||||
export type ViewsData = Awaited<ReturnType<typeof getViews>>
|
||||
export type ViewsError = unknown
|
||||
export type ViewsError = ResponseError
|
||||
|
||||
export const useViewsQuery = <TData = ViewsData>(
|
||||
{ projectRef, connectionString }: ViewsVariables,
|
||||
{ projectRef, connectionString, schema }: ViewsVariables,
|
||||
{ enabled = true, ...options }: UseQueryOptions<ViewsData, ViewsError, TData> = {}
|
||||
) =>
|
||||
useQuery<ViewsData, ViewsError, TData>(
|
||||
viewKeys.list(projectRef),
|
||||
({ signal }) => getViews({ projectRef, connectionString }, signal),
|
||||
{ enabled: enabled && typeof projectRef !== 'undefined', ...options }
|
||||
schema ? viewKeys.listBySchema(projectRef, schema) : viewKeys.list(projectRef),
|
||||
({ signal }) => getViews({ projectRef, connectionString, schema }, signal),
|
||||
{
|
||||
enabled: enabled && typeof projectRef !== 'undefined',
|
||||
...options,
|
||||
}
|
||||
)
|
||||
|
||||
export const useViewsPrefetch = ({ projectRef, connectionString, schema }: ViewsVariables) => {
|
||||
const client = useQueryClient()
|
||||
|
||||
return useCallback(() => {
|
||||
if (projectRef) {
|
||||
client.prefetchQuery(
|
||||
schema ? viewKeys.listBySchema(projectRef, schema) : viewKeys.list(projectRef),
|
||||
({ signal }) => getViews({ projectRef, connectionString, schema }, signal)
|
||||
)
|
||||
}
|
||||
}, [projectRef, schema])
|
||||
}
|
||||
@@ -37,7 +37,6 @@ import { tableKeys } from 'data/tables/keys'
|
||||
import { getTable } from 'data/tables/table-query'
|
||||
import { getTables } from 'data/tables/tables-query'
|
||||
import PostgresMetaInterface from '../common/PostgresMetaInterface'
|
||||
import ViewStore, { IViewStore } from './ViewStore'
|
||||
|
||||
const BATCH_SIZE = 1000
|
||||
const CHUNK_SIZE = 1024 * 1024 * 0.1 // 0.1MB
|
||||
@@ -45,9 +44,9 @@ const CHUNK_SIZE = 1024 * 1024 * 0.1 // 0.1MB
|
||||
export interface IMetaStore {
|
||||
tables: ITableStore
|
||||
columns: IPostgresMetaInterface<PostgresColumn>
|
||||
views: IViewStore
|
||||
|
||||
projectRef?: string
|
||||
connectionString?: string
|
||||
|
||||
query: (value: string) => Promise<any | { error: ResponseError }>
|
||||
validateQuery: (value: string) => Promise<any | { error: ResponseError }>
|
||||
@@ -117,7 +116,6 @@ export default class MetaStore implements IMetaStore {
|
||||
rootStore: IRootStore
|
||||
tables: TableStore
|
||||
columns: PostgresMetaInterface<PostgresColumn>
|
||||
views: ViewStore
|
||||
|
||||
projectRef?: string
|
||||
connectionString?: string
|
||||
@@ -142,7 +140,6 @@ export default class MetaStore implements IMetaStore {
|
||||
`${this.baseUrl}/columns`,
|
||||
this.headers
|
||||
)
|
||||
this.views = new ViewStore(this.rootStore, `${this.baseUrl}/views`, this.headers)
|
||||
|
||||
makeObservable(this, {})
|
||||
}
|
||||
@@ -832,8 +829,5 @@ export default class MetaStore implements IMetaStore {
|
||||
|
||||
this.columns.setUrl(`${this.baseUrl}/columns`)
|
||||
this.columns.setHeaders(this.headers)
|
||||
|
||||
this.views.setUrl(`${this.baseUrl}/views`)
|
||||
this.views.setHeaders(this.headers)
|
||||
}
|
||||
}
|
||||
@@ -1,69 +0,0 @@
|
||||
import { ResponseError, SchemaView } from 'types'
|
||||
import PostgresMetaInterface, { IPostgresMetaInterface } from '../common/PostgresMetaInterface'
|
||||
import { IRootStore } from '../RootStore'
|
||||
import { get } from 'lib/common/fetch'
|
||||
import { PostgresColumn, PostgresView } from '@supabase/postgres-meta'
|
||||
|
||||
export interface IViewStore extends IPostgresMetaInterface<SchemaView> {
|
||||
loadById: (id: number | string) => Promise<Partial<SchemaView> | { error: ResponseError }>
|
||||
}
|
||||
|
||||
export default class ViewStore extends PostgresMetaInterface<SchemaView> {
|
||||
constructor(
|
||||
rootStore: IRootStore,
|
||||
dataUrl: string,
|
||||
headers?: {
|
||||
[prop: string]: any
|
||||
},
|
||||
options?: { identifier: string }
|
||||
) {
|
||||
super(rootStore, dataUrl, headers, options)
|
||||
}
|
||||
|
||||
// Customize ViewStore fetchData method to improve request performance
|
||||
async fetchData() {
|
||||
const headers = { 'Content-Type': 'application/json', ...this.headers }
|
||||
// load all views w/o columns info
|
||||
const _url = new URL(this.url)
|
||||
_url.searchParams.set('include_columns', 'false')
|
||||
const url = `${_url}`
|
||||
// load all columns
|
||||
const urlColumns = this.url.replace('/views', '/columns')
|
||||
const [viewsResponse, columnsResponse] = await Promise.all([
|
||||
get(url, { headers }),
|
||||
get(urlColumns, { headers }),
|
||||
])
|
||||
if (viewsResponse.error) throw viewsResponse.error
|
||||
if (columnsResponse.error) throw columnsResponse.error
|
||||
|
||||
// merge 2 response to create the final array
|
||||
const columnsByTableId = (columnsResponse as PostgresColumn[]).reduce((acc, curr) => {
|
||||
acc[curr.table_id] ??= []
|
||||
acc[curr.table_id].push(curr)
|
||||
return acc
|
||||
}, {} as Record<string, PostgresColumn[]>)
|
||||
const views: PostgresView[] = []
|
||||
viewsResponse.forEach((view: PostgresView) => {
|
||||
const columns = columnsByTableId[view.id]
|
||||
views.push({ ...view, columns })
|
||||
})
|
||||
|
||||
this.setDataArray(views as any)
|
||||
return views as any
|
||||
}
|
||||
|
||||
async loadById(id: number | string) {
|
||||
try {
|
||||
const url = `${this.url}?id=${id}`
|
||||
const response = await get(url, { headers: this.headers })
|
||||
if (response.error) throw response.error
|
||||
|
||||
const data = response as Partial<SchemaView>
|
||||
// @ts-ignore
|
||||
this.data[id] = data
|
||||
return data
|
||||
} catch (error: any) {
|
||||
return { error }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,8 @@
|
||||
import type { PostgresColumn } from '@supabase/postgres-meta'
|
||||
import { Query } from 'components/grid/query/Query'
|
||||
import { getViews } from 'data/views/views-query'
|
||||
import { makeAutoObservable } from 'mobx'
|
||||
import { SchemaView, VaultSecret } from 'types'
|
||||
import { VaultSecret } from 'types'
|
||||
import { IRootStore } from '../RootStore'
|
||||
|
||||
export interface IVaultStore {
|
||||
@@ -211,10 +212,12 @@ export default class VaultStore implements IVaultStore {
|
||||
async listEncryptedColumns(schema: string, table: string) {
|
||||
if (!table) return []
|
||||
|
||||
await this.rootStore.meta.views.loadBySchema(schema)
|
||||
const decryptedView = this.rootStore.meta.views.find(
|
||||
(view: SchemaView) => view.name === `decrypted_${table}`
|
||||
)
|
||||
const views = await getViews({
|
||||
projectRef: this.rootStore.meta.projectRef,
|
||||
connectionString: this.rootStore.meta.connectionString,
|
||||
schema,
|
||||
})
|
||||
const decryptedView = views.find((view) => view.name === `decrypted_${table}`)
|
||||
if (!decryptedView) return []
|
||||
|
||||
const encryptedColumns = await this.rootStore.meta.query(
|
||||
|
||||
Reference in new issue
Block a user