start work on table data

This commit is contained in:
Sidney Alcantara
2022-05-04 18:24:19 +10:00
parent 2f15f57c9b
commit e30efda4d9
31 changed files with 368 additions and 97 deletions

View File

@@ -1 +1 @@
{"kind":"identitytoolkit#DownloadAccountResponse","users":[{"localId":"26CJMrwlouNRwkiLofNK07DNgKhw","createdAt":"1651022832613","lastLoginAt":"1651297974462","displayName":"Admin User","photoUrl":"","customAttributes":"{\"roles\": [\"ADMIN\"]}","providerUserInfo":[{"providerId":"google.com","rawId":"abc123","federatedId":"abc123","displayName":"Admin User","email":"admin@example.com"}],"validSince":"1651195467","email":"admin@example.com","emailVerified":true,"disabled":false,"lastRefreshAt":"2022-04-30T08:44:58.158Z"},{"localId":"3xTRVPnJGT2GE6lkiWKZp1jShuXj","createdAt":"1651023059442","lastLoginAt":"1651223181908","displayName":"Editor User","providerUserInfo":[{"providerId":"google.com","rawId":"1535779573397289142795231390488730790451","federatedId":"1535779573397289142795231390488730790451","displayName":"Editor User","email":"editor@example.com"}],"validSince":"1651195467","email":"editor@example.com","emailVerified":true,"disabled":false,"lastRefreshAt":"2022-04-30T08:44:53.855Z"}]}
{"kind":"identitytoolkit#DownloadAccountResponse","users":[{"localId":"26CJMrwlouNRwkiLofNK07DNgKhw","createdAt":"1651022832613","lastLoginAt":"1651630548960","displayName":"Admin User","photoUrl":"","customAttributes":"{\"roles\": [\"ADMIN\"]}","providerUserInfo":[{"providerId":"google.com","rawId":"abc123","federatedId":"abc123","displayName":"Admin User","email":"admin@example.com"}],"validSince":"1651630530","email":"admin@example.com","emailVerified":true,"disabled":false,"lastRefreshAt":"2022-05-04T02:15:48.960Z"},{"localId":"3xTRVPnJGT2GE6lkiWKZp1jShuXj","createdAt":"1651023059442","lastLoginAt":"1651223181908","displayName":"Editor User","providerUserInfo":[{"providerId":"google.com","rawId":"1535779573397289142795231390488730790451","federatedId":"1535779573397289142795231390488730790451","displayName":"Editor User","email":"editor@example.com"}],"validSince":"1651630530","email":"editor@example.com","emailVerified":true,"disabled":false}]}

View File

@@ -31,6 +31,8 @@ const SetupPage = lazy(() => import("@src/pages/Setup" /* webpackChunkName: "Set
// prettier-ignore
const TablesPage = lazy(() => import("@src/pages/Tables" /* webpackChunkName: "TablesPage" */));
// prettier-ignore
const TablePage = lazy(() => import("@src/pages/TableTest" /* webpackChunkName: "TablePage" */));
// prettier-ignore
const UserSettingsPage = lazy(() => import("@src/pages/Settings/UserSettings" /* webpackChunkName: "UserSettingsPage" */));
@@ -86,6 +88,11 @@ export default function App() {
/>
<Route path={ROUTES.tables} element={<TablesPage />} />
<Route path={ROUTES.table}>
<Route index element={<Navigate to={ROUTES.tables} replace />} />
<Route path=":id" element={<TablePage />} />
</Route>
<Route
path={ROUTES.settings}
element={<Navigate to={ROUTES.userSettings} replace />}

View File

@@ -3,8 +3,12 @@ import { sortBy } from "lodash-es";
import { ThemeOptions } from "@mui/material";
import { userRolesAtom } from "./auth";
import { UpdateDocFunction, UpdateCollectionFunction } from "@src/atoms/types";
import { UserSettings } from "./user";
import {
UpdateDocFunction,
UpdateCollectionFunction,
TableSettings,
} from "@src/types/table";
export const projectIdAtom = atom<string>("");
@@ -51,23 +55,6 @@ export const projectSettingsAtom = atom<ProjectSettings>({});
export const updateProjectSettingsAtom =
atom<UpdateDocFunction<ProjectSettings> | null>(null);
/** Table settings stored in project settings */
export type TableSettings = {
id: string;
collection: string;
name: string;
roles: string[];
description: string;
section: string;
tableType: "primaryCollection" | "collectionGroup";
audit?: boolean;
auditFieldCreatedBy?: string;
auditFieldUpdatedBy?: string;
readOnly?: boolean;
};
/** Tables visible to the signed-in user based on roles */
export const tablesAtom = atom<TableSettings[]>((get) => {
const userRoles = get(userRolesAtom);

View File

@@ -72,7 +72,7 @@ export const rowyRunAtom = atom((get) => {
handleNotSetUp,
}: IRowyRunRequestProps): Promise<Response | any | false> => {
if (!currentUser) {
console.log("Rowy Run: Not signed in");
console.log("Rowy Run: Not signed in", route.path);
if (handleNotSetUp) handleNotSetUp();
return false;
}
@@ -84,7 +84,7 @@ export const rowyRunAtom = atom((get) => {
? rowyRunServices?.[service]
: rowyRunUrl;
if (!serviceUrl) {
console.log("Rowy Run: Not set up");
console.log("Rowy Run: Not set up", route.path);
if (handleNotSetUp) handleNotSetUp();
return false;
}

View File

@@ -2,7 +2,7 @@ import { atom } from "jotai";
import { atomWithStorage } from "jotai/utils";
import { DialogProps, ButtonProps } from "@mui/material";
import { TableSettings } from "./project";
import { TableSettings } from "@src/types/table";
/** Nav open state stored in local storage. */
export const navOpenAtom = atomWithStorage("__ROWY__NAV_OPEN", false);

View File

@@ -5,8 +5,7 @@ import { ThemeOptions } from "@mui/material";
import themes from "@src/theme";
import { publicSettingsAtom } from "./project";
import { TableFilter } from "@src/atoms/tableScope/table";
import { UpdateDocFunction } from "@src/atoms/types";
import { UpdateDocFunction, TableFilter } from "@src/types/table";
/** User info and settings */
export type UserSettings = Partial<{

View File

@@ -1,7 +1,18 @@
import { where } from "firebase/firestore";
import { atom } from "jotai";
import {
TableSettings,
TableSchema,
TableFilter,
TableOrder,
} from "@src/types/table";
export type TableFilter = {
key: Parameters<typeof where>[0];
operator: Parameters<typeof where>[1];
value: Parameters<typeof where>[2];
};
export const tableIdAtom = atom<string | undefined>(undefined);
export const tableSettingsAtom = atom<TableSettings | undefined>(undefined);
export const tableSchemaAtom = atom<TableSchema | undefined>(undefined);
export const tableFiltersAtom = atom<TableFilter[]>([]);
export const tableOrdersAtom = atom<TableOrder[]>([]);
export const tablePageAtom = atom(0);
export const tableRowsAtom = atom<Record<string, any>[]>([]);
export const tableLoadingMoreAtom = atom(false);

View File

@@ -1,6 +0,0 @@
export type UpdateDocFunction<T> = (update: Partial<T>) => Promise<void>;
export type UpdateCollectionFunction<T> = (
path: string,
update: Partial<T>
) => Promise<void>;

View File

@@ -0,0 +1,48 @@
import { Fade, Stack, Skeleton, Button } from "@mui/material";
import AddColumnIcon from "@src/assets/icons/AddColumn";
const NUM_CELLS = 5;
export default function HeaderRowSkeleton() {
return (
<Fade in timeout={1000} style={{ transitionDelay: "1s" }} unmountOnExit>
<Stack
direction="row"
alignItems="center"
sx={{
marginLeft: (theme) =>
`max(env(safe-area-inset-left), ${theme.spacing(2)})`,
marginRight: `env(safe-area-inset-right)`,
}}
>
{new Array(NUM_CELLS + 1).fill(undefined).map((_, i) => (
<Skeleton
key={i}
variant="rectangular"
sx={{
bgcolor: "background.default",
border: "1px solid",
borderColor: "divider",
borderLeftWidth: i === 0 ? 1 : 0,
width: i === NUM_CELLS ? 46 : 150,
height: 42,
borderRadius: i === NUM_CELLS ? 1 : 0,
borderTopLeftRadius:
i === 0 ? (theme) => theme.shape.borderRadius : 0,
borderBottomLeftRadius:
i === 0 ? (theme) => theme.shape.borderRadius : 0,
}}
/>
))}
<Skeleton
sx={{ transform: "none", ml: (-46 + 6) / 8, borderRadius: 1 }}
>
<Button variant="contained" startIcon={<AddColumnIcon />}>
Add column
</Button>
</Skeleton>
</Stack>
</Fade>
);
}

View File

@@ -0,0 +1,60 @@
import { Fade, Stack, Button, Skeleton, SkeletonProps } from "@mui/material";
import AddRowIcon from "@src/assets/icons/AddRow";
// TODO:
// import { TABLE_HEADER_HEIGHT } from "@src/components/TableHeader";
const TABLE_HEADER_HEIGHT = 44;
const ButtonSkeleton = (props: Partial<SkeletonProps>) => (
<Skeleton
variant="rectangular"
{...props}
sx={{ borderRadius: 1, ...props.sx }}
/>
);
export default function TableHeaderSkeleton() {
return (
<Fade in timeout={1000} style={{ transitionDelay: "1s" }} unmountOnExit>
<Stack
direction="row"
alignItems="center"
spacing={1}
sx={{
ml: "env(safe-area-inset-left)",
mr: "env(safe-area-inset-right)",
pl: 2,
pr: 2,
pb: 1.5,
height: TABLE_HEADER_HEIGHT,
}}
>
<ButtonSkeleton>
<Button variant="contained" startIcon={<AddRowIcon />}>
Add row
</Button>
</ButtonSkeleton>
<div />
<ButtonSkeleton>
<Button variant="contained" startIcon={<AddRowIcon />}>
Hide
</Button>
</ButtonSkeleton>
<ButtonSkeleton>
<Button variant="contained" startIcon={<AddRowIcon />}>
Filter
</Button>
</ButtonSkeleton>
<div style={{ flexGrow: 1 }} />
<ButtonSkeleton style={{ width: 40, height: 32 }} />
<div />
<ButtonSkeleton style={{ width: 40, height: 32 }} />
<ButtonSkeleton style={{ width: 40, height: 32 }} />
</Stack>
</Fade>
);
}

View File

@@ -6,11 +6,8 @@ import { useSnackbar } from "notistack";
import { IconButton, Menu, MenuItem, DialogContentText } from "@mui/material";
import DeleteIcon from "@mui/icons-material/DeleteOutlined";
import {
globalScope,
confirmDialogAtom,
TableSettings,
} from "@src/atoms/globalScope";
import { globalScope, confirmDialogAtom } from "@src/atoms/globalScope";
import { TableSettings } from "@src/types/table";
import { ROUTES } from "@src/constants/routes";
import { analytics, logEvent } from "@src/analytics";

View File

@@ -23,8 +23,8 @@ import {
rolesAtom,
rowyRunAtom,
confirmDialogAtom,
TableSettings,
} from "@src/atoms/globalScope";
import { TableSettings } from "@src/types/table";
import { analytics, logEvent } from "@src/analytics";
// TODO:

View File

@@ -11,7 +11,7 @@ import {
import GoIcon from "@src/assets/icons/Go";
import RenderedMarkdown from "@src/components/RenderedMarkdown";
import { TableSettings } from "@src/atoms/globalScope";
import { TableSettings } from "@src/types/table";
export interface ITableCardProps extends TableSettings {
link: string;

View File

@@ -6,7 +6,7 @@ import SectionHeading from "@src/components/SectionHeading";
import TableCard from "./TableCard";
import SlideTransition from "@src/components/Modal/SlideTransition";
import { TableSettings } from "@src/atoms/globalScope";
import { TableSettings } from "@src/types/table";
export interface ITableGridProps {
sections: Record<string, TableSettings[]>;

View File

@@ -6,7 +6,7 @@ import SectionHeading from "@src/components/SectionHeading";
import TableListItem from "./TableListItem";
import SlideTransition from "@src/components/Modal/SlideTransition";
import { TableSettings } from "@src/atoms/globalScope";
import { TableSettings } from "@src/types/table";
export interface ITableListProps {
sections: Record<string, TableSettings[]>;

View File

@@ -9,7 +9,7 @@ import {
import GoIcon from "@mui/icons-material/ArrowForward";
import RenderedMarkdown from "@src/components/RenderedMarkdown";
import { TableSettings } from "@src/atoms/globalScope";
import { TableSettings } from "@src/types/table";
export interface ITableListItemProps extends TableSettings {
link: string;

View File

@@ -43,6 +43,8 @@ export const ROUTE_TITLES = {
),
},
[ROUTES.table]: "Table Test",
[ROUTES.settings]: "Settings",
[ROUTES.userSettings]: "Settings",
[ROUTES.projectSettings]: "Project Settings",

View File

@@ -17,7 +17,11 @@ import {
import { useErrorHandler } from "react-error-boundary";
import { globalScope } from "@src/atoms/globalScope";
import { UpdateCollectionFunction } from "@src/atoms/types";
import {
UpdateCollectionFunction,
TableFilter,
TableOrder,
} from "@src/types/table";
import { firebaseDbAtom } from "@src/sources/ProjectSourceFirebase";
/** Options for {@link useFirestoreCollectionWithAtom} */
@@ -25,9 +29,9 @@ interface IUseFirestoreCollectionWithAtomOptions<T> {
/** Additional path segments appended to the path. If any are undefined, the listener isnt created at all. */
pathSegments?: Array<string | undefined>;
/** Attach filters to the query */
filters?: Parameters<typeof where>[];
filters?: TableFilter[];
/** Attach orders to the query */
orders?: Parameters<typeof orderBy>[];
orders?: TableOrder[];
/** Called when an error occurs. Make sure to wrap in useCallback! If not provided, errors trigger the nearest ErrorBoundary. */
onError?: (error: FirestoreError) => void;
/** Optionally disable Suspense */
@@ -91,8 +95,10 @@ export function useFirestoreCollectionWithAtom<T = DocumentData>(
// Create the query with filters and orders
const _query = query<T>(
collectionRef,
...(filters?.map((filter) => where(...filter)) || []),
...(orders?.map((order) => orderBy(...order)) || [])
...(filters?.map((filter) =>
where(filter.key, filter.operator, filter.value)
) || []),
...(orders?.map((order) => orderBy(order.key, order.direction)) || [])
);
const unsubscribe = onSnapshot(

View File

@@ -13,7 +13,7 @@ import {
import { useErrorHandler } from "react-error-boundary";
import { globalScope } from "@src/atoms/globalScope";
import { UpdateDocFunction } from "@src/atoms/types";
import { UpdateDocFunction } from "@src/types/table";
import { firebaseDbAtom } from "@src/sources/ProjectSourceFirebase";
/** Options for {@link useFirestoreDocWithAtom} */

View File

@@ -31,15 +31,14 @@ import {
userRolesAtom,
userSettingsAtom,
tablesAtom,
TableSettings,
tableSettingsDialogAtom,
} from "@src/atoms/globalScope";
import { TableSettings } from "@src/types/table";
import { ROUTES } from "@src/constants/routes";
export const NAV_DRAWER_WIDTH = 256;
export interface INavDrawerProps extends DrawerProps {
currentSection?: string;
onClose: NonNullable<DrawerProps["onClose"]>;
pinned: boolean;
setPinned: React.Dispatch<React.SetStateAction<boolean>>;
@@ -142,7 +141,7 @@ export default function NavDrawer({
<nav>
<List disablePadding>
<li>
<NavItem to={ROUTES.home} onClick={closeDrawer}>
<NavItem to={ROUTES.tables} onClick={closeDrawer}>
<ListItemIcon>
<HomeIcon />
</ListItemIcon>

View File

@@ -5,32 +5,35 @@ import { List, ListItemText, Collapse } from "@mui/material";
import ArrowDropDownIcon from "@mui/icons-material/ArrowDropDown";
import NavItem from "./NavItem";
import { TableSettings } from "@src/atoms/globalScope";
import { TableSettings } from "@src/types/table";
import { ROUTES } from "@src/constants/routes";
export interface INavDrawerItemProps {
open?: boolean;
const getTableRoute = (table: TableSettings) =>
table.tableType === "collectionGroup"
? `${ROUTES.tableGroup}/${table.id}`
: `${ROUTES.table}/${table.id.replace(/\//g, "~2F")}`;
export interface INavTableSectionProps {
section: string;
tables: TableSettings[];
currentSection?: string;
closeDrawer?: (e: {}) => void;
}
export default function NavDrawerItem({
open: openProp,
export default function NavTableSection({
section,
tables,
currentSection,
closeDrawer,
}: INavDrawerItemProps) {
}: INavTableSectionProps) {
const { pathname } = useLocation();
const [open, setOpen] = useState(openProp || section === currentSection);
const hasMatch = tables.map(getTableRoute).includes(pathname);
const [open, setOpen] = useState(hasMatch);
return (
<li>
<NavItem
{...({ component: "button" } as any)}
selected={!open && currentSection === section}
selected={hasMatch && !open}
onClick={() => setOpen((o) => !o)}
>
<ListItemText primary={section} style={{ textAlign: "left" }} />
@@ -46,31 +49,25 @@ export default function NavDrawerItem({
<Collapse in={open}>
<List disablePadding>
{tables
.filter((x) => x)
.map((table) => {
const route =
table.tableType === "collectionGroup"
? `${ROUTES.tableGroup}/${table.id}`
: `${ROUTES.table}/${table.id.replace(/\//g, "~2F")}`;
{tables.map((table) => {
const route = getTableRoute(table);
return (
<li key={table.id}>
<NavItem
to={route}
selected={pathname.split("%2F")[0] === route}
onClick={closeDrawer}
sx={{
ml: 2,
width: (theme) =>
`calc(100% - ${theme.spacing(2 + 0.5)})`,
}}
>
<ListItemText primary={table.name} />
</NavItem>
</li>
);
})}
return (
<li key={table.id}>
<NavItem
to={route}
selected={pathname.split("%2F")[0] === route}
onClick={closeDrawer}
sx={{
ml: 2,
width: (theme) => `calc(100% - ${theme.spacing(2 + 0.5)})`,
}}
>
<ListItemText primary={table.name} />
</NavItem>
</li>
);
})}
</List>
</Collapse>
</li>

View File

@@ -49,7 +49,11 @@ export default function Navigation({ children }: React.PropsWithChildren<{}>) {
const canPin = !useMediaQuery((theme: any) => theme.breakpoints.down("lg"));
const { pathname } = useLocation();
const routeTitle = ROUTE_TITLES[pathname as keyof typeof ROUTE_TITLES] || "";
const basePath = ("/" + pathname.split("/")[1]) as keyof typeof ROUTE_TITLES;
const routeTitle =
ROUTE_TITLES[pathname as keyof typeof ROUTE_TITLES] ||
ROUTE_TITLES[basePath] ||
"";
const title = typeof routeTitle === "string" ? routeTitle : routeTitle.title;
useDocumentTitle(projectId, title);

View File

@@ -45,11 +45,7 @@ export default function UserMenu(props: IconButtonProps) {
const avatarUrl = userSettings.user?.photoURL;
const email = userSettings.user?.email;
const avatar = avatarUrl ? (
<Avatar src={avatarUrl} />
) : (
<AccountCircleIcon color="secondary" />
);
const avatar = avatarUrl ? <Avatar src={avatarUrl} /> : <AccountCircleIcon />;
const changeTheme = (option: "system" | "light" | "dark") => {
if (option === "system") {

49
src/pages/TableTest.tsx Normal file
View File

@@ -0,0 +1,49 @@
import { Suspense } from "react";
import { useAtom, Provider } from "jotai";
import { useParams } from "react-router-dom";
import {
tableScope,
tableIdAtom,
tableSettingsAtom,
tableSchemaAtom,
} from "@src/atoms/tableScope";
import TableSourceFirestore from "@src/sources/TableSourceFirestore";
import TableHeaderSkeleton from "@src/components/Table/Skeleton/TableHeaderSkeleton";
import HeaderRowSkeleton from "@src/components/Table/Skeleton/HeaderRowSkeleton";
function TableTestPage() {
const [tableId] = useAtom(tableIdAtom, tableScope);
const [tableSettings] = useAtom(tableSettingsAtom, tableScope);
const [tableSchema] = useAtom(tableSchemaAtom, tableScope);
return (
<div>
<p>Table ID: {tableId}</p>
<pre>{JSON.stringify(tableSettings, undefined, 2)}</pre>
<pre>{JSON.stringify(tableSchema, undefined, 2)}</pre>
</div>
);
}
export default function ProvidedTableTestPage() {
const { id } = useParams();
return (
<Suspense
fallback={
<>
<TableHeaderSkeleton />
<HeaderRowSkeleton />
</>
}
>
<Provider key={id} scope={tableScope} initialValues={[[tableIdAtom, id]]}>
<TableSourceFirestore />
<TableTestPage />
</Provider>
</Suspense>
);
}

View File

@@ -35,8 +35,8 @@ import {
tablesAtom,
tablesViewAtom,
tableSettingsDialogAtom,
TableSettings,
} from "@src/atoms/globalScope";
import { TableSettings } from "@src/types/table";
import { ROUTES } from "@src/constants/routes";
import useBasicSearch from "@src/hooks/useBasicSearch";
import { APP_BAR_HEIGHT } from "@src/layouts/Navigation";

View File

@@ -41,7 +41,7 @@ const envConnectEmulators =
/**
* Store Firebase config here so it can be set programmatically.
* This lets us switch between Firebase projects.
* Then app, auth, db, storage need to be derived atoms.
* Root atom from which app, auth, db, storage are derived.
*/
export const firebaseConfigAtom = atom<FirebaseOptions>(envConfig);

View File

@@ -0,0 +1,54 @@
import { memo, useMemo, useEffect } from "react";
import { useAtom, useSetAtom } from "jotai";
import { find } from "lodash-es";
import { globalScope, tablesAtom } from "@src/atoms/globalScope";
import {
tableScope,
tableIdAtom,
tableSettingsAtom,
tableSchemaAtom,
} from "@src/atoms/tableScope";
import { firebaseDbAtom } from "@src/sources/ProjectSourceFirebase";
import useFirestoreDocWithAtom from "@src/hooks/useFirestoreDocWithAtom";
// import useFirestoreCollectionWithAtom from "@src/hooks/useFirestoreCollectionWithAtom";
// import {
// globalScope,
// allUsersAtom,
// updateUserAtom,
// } from "@src/atoms/globalScope";
import { TABLE_SCHEMAS, TABLE_GROUP_SCHEMAS } from "@src/config/dbPaths";
const TableSourceFirestore = memo(function TableSourceFirestore() {
const [tables] = useAtom(tablesAtom, globalScope);
const [firebaseDb] = useAtom(firebaseDbAtom, globalScope);
// Get tableSettings from tableId and tables in globalScope
const [tableId] = useAtom(tableIdAtom, tableScope);
const setTableSettings = useSetAtom(tableSettingsAtom, tableScope);
// Store tableSettings as local const so we dont re-render
// when tableSettingsAtom is set
const tableSettings = useMemo(
() => find(tables, ["id", tableId]),
[tables, tableId]
);
// Store in tableSettingsAtom
useEffect(() => {
setTableSettings(tableSettings);
}, [tableSettings, setTableSettings]);
useFirestoreDocWithAtom(
tableSchemaAtom,
tableScope,
tableSettings?.tableType === "collectionGroup"
? TABLE_GROUP_SCHEMAS
: TABLE_SCHEMAS,
{ pathSegments: [tableId] }
);
return null;
});
export default TableSourceFirestore;

61
src/types/table.d.ts vendored Normal file
View File

@@ -0,0 +1,61 @@
import type { where, orderBy } from "firebase/firestore";
export type UpdateDocFunction<T> = (update: Partial<T>) => Promise<void>;
export type UpdateCollectionFunction<T> = (
path: string,
update: Partial<T>
) => Promise<void>;
/** Table settings stored in project settings */
export type TableSettings = {
id: string;
collection: string;
name: string;
roles: string[];
description: string;
section: string;
tableType: "primaryCollection" | "collectionGroup";
audit?: boolean;
auditFieldCreatedBy?: string;
auditFieldUpdatedBy?: string;
readOnly?: boolean;
};
/** Table schema document loaded when table or table settings dialog is open */
export type TableSchema = {
columns?: Record<string, ColumnConfig>;
rowHeight?: number;
filters?: TableFilter[];
functionConfigPath?: string;
extensionObjects?: any[];
webhooks?: any[];
};
export type ColumnConfig = {
fieldName: string;
key: string;
name: string;
type: FieldType;
index: number;
width?: number;
editable?: boolean;
config: { [key: string]: any };
[key: string]: any;
};
export type TableFilter = {
key: Parameters<typeof where>[0];
operator: Parameters<typeof where>[1];
value: Parameters<typeof where>[2];
};
export type TableOrder = {
key: Parameters<typeof orderBy>[0];
direction: Parameters<typeof orderBy>[1];
};