diff --git a/studio/components/interfaces/TableGridEditor/TableGridEditor.tsx b/studio/components/interfaces/TableGridEditor/TableGridEditor.tsx index 2f6adf8e635..5cd00a26d62 100644 --- a/studio/components/interfaces/TableGridEditor/TableGridEditor.tsx +++ b/studio/components/interfaces/TableGridEditor/TableGridEditor.tsx @@ -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' diff --git a/studio/components/layouts/TableEditorLayout/TableEditorLayout.tsx b/studio/components/layouts/TableEditorLayout/TableEditorLayout.tsx index fcd2f393b94..ea487c512ea 100644 --- a/studio/components/layouts/TableEditorLayout/TableEditorLayout.tsx +++ b/studio/components/layouts/TableEditorLayout/TableEditorLayout.tsx @@ -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) => { 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>(() => 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 ( } > - {children} + {router.isReady && id !== undefined ? isLoaded ? children : : children} ) } diff --git a/studio/components/layouts/TableEditorLayout/TableEditorMenu.tsx b/studio/components/layouts/TableEditorLayout/TableEditorMenu.tsx index d074803094f..9664b9700de 100644 --- a/studio/components/layouts/TableEditorLayout/TableEditorMenu.tsx +++ b/studio/components/layouts/TableEditorLayout/TableEditorMenu.tsx @@ -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' diff --git a/studio/data/database/foreign-key-constraints-query.ts b/studio/data/database/foreign-key-constraints-query.ts index 78006eaf5d0..d7ec6f5b339 100644 --- a/studio/data/database/foreign-key-constraints-query.ts +++ b/studio/data/database/foreign-key-constraints-query.ts @@ -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] + ) } diff --git a/studio/data/entity-types/entity-type-query.ts b/studio/data/entity-types/entity-type-query.ts new file mode 100644 index 00000000000..d1f3f8696a6 --- /dev/null +++ b/studio/data/entity-types/entity-type-query.ts @@ -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 = ( + { projectRef, connectionString, id, ...args }: EntityTypeVariables, + { enabled, ...options }: UseQueryOptions = {} +) => + 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 ( + * prefetch({ ...args })}> + * Start loading on hover + * + * ) + */ +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] + ) +} diff --git a/studio/data/entity-types/entity-types-infinite-query.ts b/studio/data/entity-types/entity-types-infinite-query.ts index 20cf2231fe3..1a0d9e7f82f 100644 --- a/studio/data/entity-types/entity-types-infinite-query.ts +++ b/studio/data/entity-types/entity-types-infinite-query.ts @@ -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 -export type Entity = { - id: number - schema: string - name: string - type: string - comment: string | null -} - export type EntityTypesResponse = { data: { entities: Entity[] diff --git a/studio/data/fdw/fdws-query.ts b/studio/data/fdw/fdws-query.ts index 0208805de29..832126b5ec2 100644 --- a/studio/data/fdw/fdws-query.ts +++ b/studio/data/fdw/fdws-query.ts @@ -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 = ( 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] + ) } diff --git a/studio/data/sql/execute-sql-query.ts b/studio/data/sql/execute-sql-query.ts index a9bfdf29700..be2f598d8a5 100644 --- a/studio/data/sql/execute-sql-query.ts +++ b/studio/data/sql/execute-sql-query.ts @@ -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 = ( { 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 ( + * prefetch({ ...args })}> + * Start loading on hover + * + * ) + */ +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] + ) } diff --git a/studio/data/table-rows/table-rows-count-query.ts b/studio/data/table-rows/table-rows-count-query.ts index bb3e504c667..396aa2818ee 100644 --- a/studio/data/table-rows/table-rows-count-query.ts +++ b/studio/data/table-rows/table-rows-count-query.ts @@ -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 = prefetch()}> + * prefetch({ ...args })}> * Start loading on hover * * ) */ -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] + ) } diff --git a/studio/data/table-rows/table-rows-query.ts b/studio/data/table-rows/table-rows-query.ts index edbf5155767..651fbe07716 100644 --- a/studio/data/table-rows/table-rows-query.ts +++ b/studio/data/table-rows/table-rows-query.ts @@ -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 = ( * * ) */ -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] + ) } diff --git a/studio/hooks/misc/useEntityType.ts b/studio/hooks/misc/useEntityType.ts new file mode 100644 index 00000000000..52211c72833 --- /dev/null +++ b/studio/hooks/misc/useEntityType.ts @@ -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 diff --git a/studio/hooks/misc/useLatest.ts b/studio/hooks/misc/useLatest.ts new file mode 100644 index 00000000000..68577e65399 --- /dev/null +++ b/studio/hooks/misc/useLatest.ts @@ -0,0 +1,9 @@ +import { useRef } from 'react' + +const useLatest = (value: T): { readonly current: T } => { + const ref = useRef(value) + ref.current = value + return ref +} + +export default useLatest diff --git a/studio/hooks/misc/useTableRowsPrefetch.ts b/studio/hooks/misc/useTableRowsPrefetch.ts new file mode 100644 index 00000000000..74dcae5e95e --- /dev/null +++ b/studio/hooks/misc/useTableRowsPrefetch.ts @@ -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 diff --git a/studio/pages/project/[ref]/editor/[id].tsx b/studio/pages/project/[ref]/editor/[id].tsx index bb8f7ea54ab..cf7756ecb7a 100644 --- a/studio/pages/project/[ref]/editor/[id].tsx +++ b/studio/pages/project/[ref]/editor/[id].tsx @@ -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' diff --git a/studio/pages/project/[ref]/editor/index.tsx b/studio/pages/project/[ref]/editor/index.tsx index 7d20614af77..23384fd610d 100644 --- a/studio/pages/project/[ref]/editor/index.tsx +++ b/studio/pages/project/[ref]/editor/index.tsx @@ -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' diff --git a/studio/stores/pgmeta/ForeignTableStore.ts b/studio/stores/pgmeta/ForeignTableStore.ts index 19c0deb4aea..70282372e01 100644 --- a/studio/stores/pgmeta/ForeignTableStore.ts +++ b/studio/stores/pgmeta/ForeignTableStore.ts @@ -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> { + loadById: (id: number | string) => Promise | { error: ResponseError }> +} + export default class ForeignTableStore extends PostgresMetaInterface> { constructor( rootStore: IRootStore, @@ -18,4 +24,19 @@ export default class ForeignTableStore extends PostgresMetaInterface + // @ts-ignore + this.data[id] = data + return data + } catch (error: any) { + return { error } + } + } } diff --git a/studio/stores/pgmeta/MetaStore.ts b/studio/stores/pgmeta/MetaStore.ts index be29ada3061..fbe99620f64 100644 --- a/studio/stores/pgmeta/MetaStore.ts +++ b/studio/stores/pgmeta/MetaStore.ts @@ -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 schemas: IPostgresMetaInterface views: IViewStore - foreignTables: IPostgresMetaInterface> + foreignTables: IForeignTableStore hooks: IPostgresMetaInterface roles: IRolesStore