Refactored queries.

This commit is contained in:
Mike Cao 2024-01-30 00:10:25 -08:00
parent 18e36aa7b3
commit b16f5cc067
67 changed files with 523 additions and 576 deletions

View File

@ -6,31 +6,30 @@ import Icons from 'components/icons';
import ThemeButton from 'components/input/ThemeButton';
import LanguageButton from 'components/input/LanguageButton';
import ProfileButton from 'components/input/ProfileButton';
import { useMessages } from 'components/hooks';
import { useMessages, useNavigation } from 'components/hooks';
import HamburgerButton from 'components/common/HamburgerButton';
import { usePathname } from 'next/navigation';
import styles from './NavBar.module.css';
export function NavBar() {
const pathname = usePathname();
const { formatMessage, labels } = useMessages();
const cloudMode = Boolean(process.env.cloudMode);
const { pathname, renderTeamUrl } = useNavigation();
const links = [
{ label: formatMessage(labels.dashboard), url: '/dashboard' },
{ label: formatMessage(labels.websites), url: '/websites' },
{ label: formatMessage(labels.reports), url: '/reports' },
{ label: formatMessage(labels.settings), url: '/settings' },
{ label: formatMessage(labels.dashboard), url: renderTeamUrl('/dashboard') },
{ label: formatMessage(labels.websites), url: renderTeamUrl('/websites') },
{ label: formatMessage(labels.reports), url: renderTeamUrl('/reports') },
{ label: formatMessage(labels.settings), url: renderTeamUrl('/settings') },
].filter(n => n);
const menuItems = [
{
label: formatMessage(labels.dashboard),
url: '/dashboard',
url: renderTeamUrl('/dashboard'),
},
!cloudMode && {
label: formatMessage(labels.settings),
url: '/settings',
url: renderTeamUrl('/settings'),
children: [
{
label: formatMessage(labels.websites),

View File

@ -5,14 +5,14 @@ async function getEnabled() {
return !!process.env.ENABLE_TEST_CONSOLE;
}
export default async function ({ params: { id } }) {
export default async function ({ params: { websiteId } }) {
const enabled = await getEnabled();
if (!enabled) {
return null;
}
return <TestConsole websiteId={id?.[0]} />;
return <TestConsole websiteId={websiteId?.[0]} />;
}
export const metadata: Metadata = {

View File

@ -3,8 +3,14 @@ import { useReports } from 'components/hooks';
import ReportsTable from './ReportsTable';
import DataTable from 'components/common/DataTable';
export default function ReportsDataTable({ websiteId }: { websiteId?: string }) {
const queryResult = useReports(websiteId);
export default function ReportsDataTable({
websiteId,
teamId,
}: {
websiteId?: string;
teamId?: string;
}) {
const queryResult = useReports({ websiteId, teamId });
return (
<DataTable queryResult={queryResult}>

View File

@ -1,23 +1,21 @@
'use client';
import PageHeader from 'components/layout/PageHeader';
import { Button, Icon, Icons, Text } from 'react-basics';
import { useMessages } from 'components/hooks';
import { useRouter } from 'next/navigation';
import { Icon, Icons, Text } from 'react-basics';
import { useMessages, useNavigation } from 'components/hooks';
import LinkButton from 'components/common/LinkButton';
export function ReportsHeader() {
const { formatMessage, labels } = useMessages();
const router = useRouter();
const handleClick = () => router.push('/reports/create');
const { renderTeamUrl } = useNavigation();
return (
<PageHeader title={formatMessage(labels.reports)}>
<Button variant="primary" onClick={handleClick}>
<LinkButton href={renderTeamUrl('/reports/create')} variant="primary">
<Icon>
<Icons.Plus />
</Icon>
<Text>{formatMessage(labels.createReport)}</Text>
</Button>
</LinkButton>
</PageHeader>
);
}

View File

@ -6,7 +6,7 @@ import Funnel from 'assets/funnel.svg';
import Lightbulb from 'assets/lightbulb.svg';
import Magnet from 'assets/magnet.svg';
import styles from './ReportTemplates.module.css';
import { useMessages } from 'components/hooks';
import { useMessages, useNavigation } from 'components/hooks';
function ReportItem({ title, description, url, icon }) {
const { formatMessage, labels } = useMessages();
@ -32,26 +32,27 @@ function ReportItem({ title, description, url, icon }) {
);
}
export function ReportTemplates({ showHeader = true }) {
export function ReportTemplates({ showHeader = true }: { showHeader?: boolean }) {
const { formatMessage, labels } = useMessages();
const { renderTeamUrl } = useNavigation();
const reports = [
{
title: formatMessage(labels.insights),
description: formatMessage(labels.insightsDescription),
url: '/reports/insights',
url: renderTeamUrl('/reports/insights'),
icon: <Lightbulb />,
},
{
title: formatMessage(labels.funnel),
description: formatMessage(labels.funnelDescription),
url: '/reports/funnel',
url: renderTeamUrl('/reports/funnel'),
icon: <Funnel />,
},
{
title: formatMessage(labels.retention),
description: formatMessage(labels.retentionDescription),
url: '/reports/retention',
url: renderTeamUrl('/reports/retention'),
icon: <Magnet />,
},
];

View File

@ -1,9 +1,10 @@
import { Metadata } from 'next';
import RetentionReport from './RetentionReport';
export default function RetentionReportPage() {
return <RetentionReport reportId={null} />;
}
export const metadata = {
export const metadata: Metadata = {
title: 'Create Report | umami',
};

View File

@ -28,7 +28,7 @@ export function LanguageSetting() {
items={options}
value={locale}
renderValue={renderValue}
onChange={saveLocale}
onChange={val => saveLocale(val as string)}
allowSearch={true}
onSearch={setSearch}
menuProps={{ className: styles.menu }}

View File

@ -7,7 +7,7 @@ import {
Button,
SubmitButton,
} from 'react-basics';
import { setValue } from 'store/cache';
import { touch } from 'store/cache';
import { useApi, useMessages } from 'components/hooks';
export function TeamAddForm({ onSave, onClose }: { onSave: () => void; onClose: () => void }) {
@ -17,10 +17,10 @@ export function TeamAddForm({ onSave, onClose }: { onSave: () => void; onClose:
mutationFn: (data: any) => post('/teams', data),
});
const handleSubmit = async data => {
const handleSubmit = async (data: any) => {
mutate(data, {
onSuccess: async () => {
setValue('teams', Date.now());
touch('teams');
onSave?.();
onClose?.();
},

View File

@ -9,7 +9,7 @@ import {
SubmitButton,
} from 'react-basics';
import { useApi, useMessages } from 'components/hooks';
import { setValue } from 'store/cache';
import { touch } from 'store/cache';
export function TeamJoinForm({ onSave, onClose }: { onSave: () => void; onClose: () => void }) {
const { formatMessage, labels, getMessage } = useMessages();
@ -20,7 +20,7 @@ export function TeamJoinForm({ onSave, onClose }: { onSave: () => void; onClose:
const handleSubmit = async (data: any) => {
mutate(data, {
onSuccess: async () => {
setValue('teams:members', Date.now());
touch('teams:members');
onSave?.();
onClose?.();
},

View File

@ -18,11 +18,11 @@ const generateId = () => getRandomChars(16);
export function TeamEditForm({
teamId,
data,
readOnly,
allowEdit,
}: {
teamId: string;
data?: { name: string; accessCode: string };
readOnly?: boolean;
allowEdit?: boolean;
}) {
const { formatMessage, labels, messages } = useMessages();
const { post, useMutation } = useApi();
@ -57,22 +57,24 @@ export function TeamEditForm({
<TextField value={teamId} readOnly allowCopy />
</FormRow>
<FormRow label={formatMessage(labels.name)}>
{!readOnly && (
{allowEdit && (
<FormInput name="name" rules={{ required: formatMessage(labels.required) }}>
<TextField />
</FormInput>
)}
{readOnly && data.name}
{!allowEdit && data.name}
</FormRow>
<FormRow label={formatMessage(labels.accessCode)}>
<Flexbox gap={10}>
<TextField value={accessCode} readOnly allowCopy />
{!readOnly && (
<Button onClick={handleRegenerate}>{formatMessage(labels.regenerate)}</Button>
)}
</Flexbox>
</FormRow>
{!readOnly && (
{allowEdit && (
<FormRow label={formatMessage(labels.accessCode)}>
<Flexbox gap={10}>
<TextField value={accessCode} readOnly allowCopy />
{allowEdit && (
<Button onClick={handleRegenerate}>{formatMessage(labels.regenerate)}</Button>
)}
</Flexbox>
</FormRow>
)}
{allowEdit && (
<FormButtons>
<SubmitButton variant="primary">{formatMessage(labels.save)}</SubmitButton>
</FormButtons>

View File

@ -1,6 +1,6 @@
import { useApi, useMessages } from 'components/hooks';
import { Icon, Icons, LoadingButton, Text } from 'react-basics';
import { setValue } from 'store/cache';
import { touch } from 'store/cache';
export function TeamMemberRemoveButton({
teamId,
@ -22,7 +22,7 @@ export function TeamMemberRemoveButton({
const handleRemoveTeamMember = () => {
mutate(null, {
onSuccess: () => {
setValue('team:members', Date.now());
touch('team:members');
onSave?.();
},
});

View File

@ -1,24 +1,13 @@
import { useApi, useFilterQuery } from 'components/hooks';
import DataTable from 'components/common/DataTable';
import useCache from 'store/cache';
import TeamMembersTable from './TeamMembersTable';
import useTeamMembers from 'components/hooks/queries/useTeamMembers';
export function TeamMembers({ teamId, readOnly }: { teamId: string; readOnly: boolean }) {
const { get } = useApi();
const modified = useCache(state => state?.['team:members']);
const queryResult = useFilterQuery({
queryKey: ['team:members', { teamId, modified }],
queryFn: params => {
return get(`/teams/${teamId}/users`, {
...params,
});
},
enabled: !!teamId,
});
export function TeamMembers({ teamId, allowEdit }: { teamId: string; allowEdit: boolean }) {
const queryResult = useTeamMembers(teamId);
return (
<DataTable queryResult={queryResult}>
{({ data }) => <TeamMembersTable data={data} teamId={teamId} readOnly={readOnly} />}
{({ data }) => <TeamMembersTable data={data} teamId={teamId} allowEdit={allowEdit} />}
</DataTable>
);
}

View File

@ -6,11 +6,11 @@ import TeamMemberRemoveButton from './TeamMemberRemoveButton';
export function TeamMembersTable({
data = [],
teamId,
readOnly,
allowEdit,
}: {
data: any[];
teamId: string;
readOnly: boolean;
allowEdit: boolean;
}) {
const { formatMessage, labels } = useMessages();
const { user } = useLogin();
@ -23,16 +23,20 @@ export function TeamMembersTable({
return (
<GridTable data={data} cardMode={['xs', 'sm', 'md'].includes(breakpoint)}>
<GridColumn name="username" label={formatMessage(labels.username)} />
<GridColumn name="username" label={formatMessage(labels.username)}>
{row => row?.user?.username}
</GridColumn>
<GridColumn name="role" label={formatMessage(labels.role)}>
{row => roles[row?.teamUser?.[0]?.role]}
{row => roles[row?.role]}
</GridColumn>
<GridColumn name="action" label=" " alignment="end">
{row => {
return (
!readOnly &&
row?.teamUser?.[0]?.role !== ROLES.teamOwner &&
user?.id !== row?.id && <TeamMemberRemoveButton teamId={teamId} userId={row.id} />
allowEdit &&
row?.role !== ROLES.teamOwner &&
user?.id !== row?.id && (
<TeamMemberRemoveButton teamId={teamId} userId={row?.user?.id} />
)
);
}}
</GridColumn>

View File

@ -47,9 +47,9 @@ export function TeamSettings({ teamId }: { teamId: string }) {
<Item key="websites">{formatMessage(labels.websites)}</Item>
<Item key="data">{formatMessage(labels.data)}</Item>
</Tabs>
{tab === 'details' && <TeamEditForm teamId={teamId} data={team} readOnly={!canEdit} />}
{tab === 'members' && <TeamMembers teamId={teamId} readOnly={!canEdit} />}
{tab === 'websites' && <TeamWebsites teamId={teamId} readOnly={!canEdit} />}
{tab === 'details' && <TeamEditForm teamId={teamId} data={team} allowEdit={canEdit} />}
{tab === 'members' && <TeamMembers teamId={teamId} allowEdit={canEdit} />}
{tab === 'websites' && <TeamWebsites teamId={teamId} allowEdit={canEdit} />}
{canEdit && tab === 'data' && <TeamData teamId={teamId} />}
</Flexbox>
</TeamsContext.Provider>

View File

@ -1,7 +1,7 @@
import WebsitesDataTable from 'app/(main)/settings/websites/WebsitesDataTable';
export function TeamWebsites({ teamId }: { teamId: string; readOnly: boolean }) {
return <WebsitesDataTable teamId={teamId} />;
export function TeamWebsites({ teamId, allowEdit }: { teamId: string; allowEdit: boolean }) {
return <WebsitesDataTable teamId={teamId} allowEdit={allowEdit} />;
}
export default TeamWebsites;

View File

@ -1,7 +1,7 @@
import { Button, Icon, Icons, Modal, ModalTrigger, Text, useToasts } from 'react-basics';
import WebsiteAddForm from './WebsiteAddForm';
import { useMessages } from 'components/hooks';
import { setValue } from 'store/cache';
import { touch } from 'store/cache';
export function WebsiteAddButton({ teamId, onSave }: { teamId: string; onSave?: () => void }) {
const { formatMessage, labels, messages } = useMessages();
@ -9,7 +9,7 @@ export function WebsiteAddButton({ teamId, onSave }: { teamId: string; onSave?:
const handleSave = async () => {
showToast({ message: formatMessage(messages.saved), variant: 'success' });
setValue('websites', Date.now());
touch('websites');
onSave?.();
};

View File

@ -5,6 +5,7 @@ import WebsitesHeader from './WebsitesHeader';
export default function Websites() {
const { user } = useLogin();
return (
<>
<WebsitesHeader showActions={user.role !== 'view-only'} />

View File

@ -1,7 +1,7 @@
import { ReactNode } from 'react';
import Link from 'next/link';
import { Button, Text, Icon, Icons, GridTable, GridColumn, useBreakpoint } from 'react-basics';
import { useMessages, useLogin } from 'components/hooks';
import { useMessages, useLogin, useNavigation } from 'components/hooks';
export interface WebsitesTableProps {
data: any[];
@ -23,6 +23,7 @@ export function WebsitesTable({
const { formatMessage, labels } = useMessages();
const { user } = useLogin();
const breakpoint = useBreakpoint();
const { renderTeamUrl } = useNavigation();
return (
<GridTable data={data} cardMode={['xs', 'sm', 'md'].includes(breakpoint)}>
@ -46,7 +47,7 @@ export function WebsitesTable({
</Link>
)}
{allowView && (
<Link href={teamId ? `/teams/${teamId}/websites/${id}` : `/websites/${id}`}>
<Link href={renderTeamUrl(`/websites/${id}`)}>
<Button>
<Icon>
<Icons.External />

View File

@ -1,9 +1,9 @@
import WebsiteSettings from '../WebsiteSettings';
export default async function WebsiteSettingsPage({ params: { id } }) {
if (process.env.cloudMode) {
export default async function WebsiteSettingsPage({ params: { websiteId } }) {
if (process.env.cloudMode || !websiteId) {
return null;
}
return <WebsiteSettings websiteId={id} />;
return <WebsiteSettings websiteId={websiteId} />;
}

View File

@ -0,0 +1,27 @@
'use client';
import Link from 'next/link';
import { Button, Flexbox, Icon, Icons, Text } from 'react-basics';
import { useMessages } from 'components/hooks';
import ReportsDataTable from 'app/(main)/reports/ReportsDataTable';
export function TeamReports({ websiteId }) {
const { formatMessage, labels } = useMessages();
return (
<>
<Flexbox alignItems="center" justifyContent="end">
<Link href={`/reports/create`}>
<Button variant="primary">
<Icon>
<Icons.Plus />
</Icon>
<Text>{formatMessage(labels.createReport)}</Text>
</Button>
</Link>
</Flexbox>
<ReportsDataTable websiteId={websiteId} />
</>
);
}
export default TeamReports;

View File

@ -0,0 +1,10 @@
import { Metadata } from 'next';
import ReportTemplates from 'app/(main)/reports/create/ReportTemplates';
export default function () {
return <ReportTemplates />;
}
export const metadata: Metadata = {
title: 'Create Report | umami',
};

View File

@ -0,0 +1,15 @@
import { Metadata } from 'next';
import ReportsHeader from 'app/(main)/reports/ReportsHeader';
import ReportsDataTable from 'app/(main)/reports/ReportsDataTable';
export default function ({ params: { teamId } }) {
return (
<>
<ReportsHeader />
<ReportsDataTable teamId={teamId} />
</>
);
}
export const metadata: Metadata = {
title: 'Reports | umami',
};

View File

@ -1,4 +1,4 @@
import WebsiteDetails from '../../../../websites/[websiteId]/WebsiteDetails';
import WebsiteDetails from 'app/(main)/websites/[websiteId]/WebsiteDetails';
export default function TeamWebsitePage({ params: { websiteId } }) {
return <WebsiteDetails websiteId={websiteId} />;

View File

@ -43,7 +43,7 @@ export default function WebsiteExpandedView({
const { formatMessage, labels } = useMessages();
const {
router,
makeUrl,
renderUrl,
pathname,
query: { view },
} = useNavigation();
@ -52,69 +52,69 @@ export default function WebsiteExpandedView({
{
key: 'url',
label: formatMessage(labels.pages),
url: makeUrl({ view: 'url' }),
url: renderUrl({ view: 'url' }),
},
{
key: 'referrer',
label: formatMessage(labels.referrers),
url: makeUrl({ view: 'referrer' }),
url: renderUrl({ view: 'referrer' }),
},
{
key: 'browser',
label: formatMessage(labels.browsers),
url: makeUrl({ view: 'browser' }),
url: renderUrl({ view: 'browser' }),
},
{
key: 'os',
label: formatMessage(labels.os),
url: makeUrl({ view: 'os' }),
url: renderUrl({ view: 'os' }),
},
{
key: 'device',
label: formatMessage(labels.devices),
url: makeUrl({ view: 'device' }),
url: renderUrl({ view: 'device' }),
},
{
key: 'country',
label: formatMessage(labels.countries),
url: makeUrl({ view: 'country' }),
url: renderUrl({ view: 'country' }),
},
{
key: 'region',
label: formatMessage(labels.regions),
url: makeUrl({ view: 'region' }),
url: renderUrl({ view: 'region' }),
},
{
key: 'city',
label: formatMessage(labels.cities),
url: makeUrl({ view: 'city' }),
url: renderUrl({ view: 'city' }),
},
{
key: 'language',
label: formatMessage(labels.languages),
url: makeUrl({ view: 'language' }),
url: renderUrl({ view: 'language' }),
},
{
key: 'screen',
label: formatMessage(labels.screens),
url: makeUrl({ view: 'screen' }),
url: renderUrl({ view: 'screen' }),
},
{
key: 'event',
label: formatMessage(labels.events),
url: makeUrl({ view: 'event' }),
url: renderUrl({ view: 'event' }),
},
{
key: 'query',
label: formatMessage(labels.queryParameters),
url: makeUrl({ view: 'query' }),
url: renderUrl({ view: 'query' }),
},
];
const DetailsComponent = views[view] || (() => null);
const handleChange = (view: any) => {
router.push(makeUrl({ view }));
router.push(renderUrl({ view }));
};
const renderValue = (value: string) => items.find(({ key }) => key === value)?.label;

View File

@ -11,7 +11,7 @@ export function WebsiteFilterButton({
className?: string;
}) {
const { formatMessage, labels } = useMessages();
const { makeUrl, router } = useNavigation();
const { renderUrl, router } = useNavigation();
const fieldOptions = [
{ name: 'url', type: 'string', label: formatMessage(labels.url) },
@ -25,7 +25,7 @@ export function WebsiteFilterButton({
];
const handleAddFilter = ({ name, value }) => {
router.push(makeUrl({ [name]: value }));
router.push(renderUrl({ [name]: value }));
};
return (

View File

@ -6,7 +6,7 @@ import { DATA_TYPES } from 'lib/constants';
export function EventDataTable({ data = [] }) {
const { formatMessage, labels } = useMessages();
const { makeUrl } = useNavigation();
const { renderUrl } = useNavigation();
if (data.length === 0) {
return <Empty />;
@ -16,7 +16,7 @@ export function EventDataTable({ data = [] }) {
<GridTable data={data}>
<GridColumn name="eventName" label={formatMessage(labels.event)}>
{row => (
<Link href={makeUrl({ event: row.eventName })} shallow={true}>
<Link href={renderUrl({ event: row.eventName })} shallow={true}>
{row.eventName}
</Link>
)}

View File

@ -8,12 +8,12 @@ import { DATA_TYPES } from 'lib/constants';
export function EventDataValueTable({ data = [], event }: { data: any[]; event: string }) {
const { formatMessage, labels } = useMessages();
const { makeUrl } = useNavigation();
const { renderUrl } = useNavigation();
const Title = () => {
return (
<>
<Link href={makeUrl({ event: undefined })}>
<Link href={renderUrl({ event: undefined })}>
<Button>
<Icon rotate={180}>
<Icons.ArrowRight />

View File

@ -1,15 +1,15 @@
import WebsiteHeader from '../WebsiteHeader';
import WebsiteEventData from './WebsiteEventData';
export default function WebsiteEventDataPage({ params: { id } }) {
if (!id) {
export default function WebsiteEventDataPage({ params: { websiteId } }) {
if (!websiteId) {
return null;
}
return (
<>
<WebsiteHeader websiteId={id} />
<WebsiteEventData websiteId={id} />
<WebsiteHeader websiteId={websiteId} />
<WebsiteEventData websiteId={websiteId} />
</>
);
}

View File

@ -1,5 +1,5 @@
import WebsiteDetails from './WebsiteDetails';
export default function WebsitePage({ params: { id } }) {
return <WebsiteDetails websiteId={id} />;
export default function WebsitePage({ params: { websiteId } }) {
return <WebsiteDetails websiteId={websiteId} />;
}

View File

@ -1,9 +1,9 @@
import Realtime from './Realtime';
export default function WebsiteRealtimePage({ params: { id } }) {
if (!id) {
export default function WebsiteRealtimePage({ params: { websiteId } }) {
if (!websiteId) {
return null;
}
return <Realtime websiteId={id} />;
return <Realtime websiteId={websiteId} />;
}

View File

@ -1,9 +1,9 @@
import WebsiteReports from './WebsiteReports';
export default function WebsiteReportsPage({ params: { id } }) {
if (!id) {
export default function WebsiteReportsPage({ params: { websiteId } }) {
if (!websiteId) {
return null;
}
return <WebsiteReports websiteId={id} />;
return <WebsiteReports websiteId={websiteId} />;
}

View File

@ -25,7 +25,7 @@ export function FilterLink({
className,
}: FilterLinkProps) {
const { formatMessage, labels } = useMessages();
const { makeUrl, query } = useNavigation();
const { renderUrl, query } = useNavigation();
const active = query[id] !== undefined;
const selected = query[id] === value;
@ -39,7 +39,7 @@ export function FilterLink({
{children}
{!value && `(${label || formatMessage(labels.unknown)})`}
{value && (
<Link href={makeUrl({ [id]: value })} className={styles.label} replace>
<Link href={renderUrl({ [id]: value })} className={styles.label} replace>
{safeDecodeURI(label || value)}
</Link>
)}

View File

@ -1,23 +1,23 @@
import { useState } from 'react';
import useApi from './useApi';
import useFilterQuery from './useFilterQuery';
import useCache from 'store/cache';
export function useReports(websiteId?: string) {
const [modified, setModified] = useState(Date.now());
export function useReports({ websiteId, teamId }: { websiteId?: string; teamId?: string }) {
const modified = useCache((state: any) => state?.reports);
const { get, del, useMutation } = useApi();
const { mutate } = useMutation({ mutationFn: (reportId: string) => del(`/reports/${reportId}`) });
const queryResult = useFilterQuery({
queryKey: ['reports', { websiteId, modified }],
queryFn: (params: any) => {
return get(websiteId ? `/websites/${websiteId}/reports` : `/reports`, params);
const url = websiteId ? `/websites/${websiteId}/reports` : `/reports`;
return get(teamId ? `/teams/${teamId}${url}` : url, params);
},
});
const { mutate } = useMutation({ mutationFn: (reportId: string) => del(`/reports/${reportId}`) });
const deleteReport = (id: any) => {
mutate(id, {
onSuccess: () => {
setModified(Date.now());
},
const deleteReport = (reportId: any) => {
mutate(reportId, {
onSuccess: () => {},
});
};

View File

@ -0,0 +1,16 @@
import useApi from './useApi';
import useFilterQuery from './useFilterQuery';
export function useTeamMembers(teamId: string) {
const { get } = useApi();
return useFilterQuery({
queryKey: ['teams:users', { teamId }],
queryFn: (params: any) => {
return get(`/teams/${teamId}/users`, params);
},
enabled: !!teamId,
});
}
export default useTeamMembers;

View File

@ -10,7 +10,7 @@ const messages = {
'en-US': enUS,
};
const selector = state => state.locale;
const selector = (state: { locale: any }) => state.locale;
export function useLocale() {
const locale = useStore(selector);
@ -18,7 +18,7 @@ export function useLocale() {
const dir = getTextDirection(locale);
const dateLocale = getDateLocale(locale);
async function loadMessages(locale) {
async function loadMessages(locale: string) {
const { ok, data } = await httpGet(`${process.env.basePath}/intl/messages/${locale}.json`);
if (ok) {
@ -26,7 +26,7 @@ export function useLocale() {
}
}
async function saveLocale(value) {
async function saveLocale(value: string) {
if (!messages[value]) {
await loadMessages(value);
}

View File

@ -6,11 +6,14 @@ export function useNavigation(): {
pathname: string;
query: { [key: string]: string };
router: any;
makeUrl: (params: any, reset?: boolean) => string;
renderUrl: (params: any, reset?: boolean) => string;
renderTeamUrl: (url: string) => string;
teamId?: string;
} {
const router = useRouter();
const pathname = usePathname();
const params = useSearchParams();
const [, teamId] = pathname.match(/^\/teams\/([a-f0-9-]+)/) || [];
const query = useMemo(() => {
const obj = {};
@ -22,11 +25,15 @@ export function useNavigation(): {
return obj;
}, [params]);
function makeUrl(params: any, reset?: boolean) {
function renderUrl(params: any, reset?: boolean) {
return reset ? pathname : buildUrl(pathname, { ...query, ...params });
}
return { pathname, query, router, makeUrl };
function renderTeamUrl(url: string) {
return teamId ? `/teams/${teamId}${url}` : url;
}
return { pathname, query, router, renderUrl, renderTeamUrl, teamId };
}
export default useNavigation;

View File

@ -10,7 +10,7 @@ export function FilterTags({ params }) {
const { formatValue } = useFormat();
const {
router,
makeUrl,
renderUrl,
query: { view },
} = useNavigation();
@ -19,11 +19,11 @@ export function FilterTags({ params }) {
}
function handleCloseFilter(param?: string) {
router.push(makeUrl({ [param]: undefined }));
router.push(renderUrl({ [param]: undefined }));
}
function handleResetFilter() {
router.push(makeUrl({ view }, true));
router.push(renderUrl({ view }, true));
}
return (

View File

@ -45,7 +45,7 @@ export function MetricsTable({
const { formatValue } = useFormat();
const [{ startDate, endDate, modified }] = useDateRange(websiteId);
const {
makeUrl,
renderUrl,
query: { url, referrer, title, os, browser, device, country, region, city },
} = useNavigation();
const { formatMessage, labels } = useMessages();
@ -142,7 +142,7 @@ export function MetricsTable({
{!data && isLoading && !isFetched && <Loading icon="dots" />}
<div className={styles.footer}>
{data && !error && limit && (
<LinkButton href={makeUrl({ view: type })} variant="quiet">
<LinkButton href={renderUrl({ view: type })} variant="quiet">
<Text>{formatMessage(labels.more)}</Text>
<Icon size="sm" rotate={dir === 'rtl' ? 180 : 0}>
<Icons.ArrowRight />

View File

@ -12,13 +12,13 @@ export interface PagesTableProps extends MetricsTableProps {
export function PagesTable({ allowFilter, domainName, ...props }: PagesTableProps) {
const {
router,
makeUrl,
renderUrl,
query: { view = 'url' },
} = useNavigation();
const { formatMessage, labels } = useMessages();
const handleSelect = (key: any) => {
router.push(makeUrl({ view: key }), { scroll: true });
router.push(renderUrl({ view: key }), { scroll: true });
};
const buttons = [

View File

@ -1,12 +1,12 @@
import { User, Website } from '@prisma/client';
import redis from '@umami/redis-client';
import { getSession, getUserById, getWebsiteById } from '../queries';
import { getSession, getUser, getWebsite } from '../queries';
async function fetchWebsite(id): Promise<Website> {
return redis.client.getCache(`website:${id}`, () => getWebsiteById(id), 86400);
async function fetchWebsite(websiteId: string): Promise<Website> {
return redis.client.getCache(`website:${websiteId}`, () => getWebsite(websiteId), 86400);
}
async function storeWebsite(data) {
async function storeWebsite(data: { id: any }) {
const { id } = data;
const key = `website:${id}`;
@ -21,11 +21,7 @@ async function deleteWebsite(id) {
}
async function fetchUser(id): Promise<User> {
return redis.client.getCache(
`user:${id}`,
() => getUserById(id, { includePassword: true }),
86400,
);
return redis.client.getCache(`user:${id}`, () => getUser(id, { includePassword: true }), 86400);
}
async function storeUser(data) {

View File

@ -1,5 +1,5 @@
import cache from 'lib/cache';
import { getSession, getUserById, getWebsiteById } from 'queries';
import { getSession, getUser, getWebsite } from 'queries';
import { User, Website, Session } from '@prisma/client';
export async function loadWebsite(websiteId: string): Promise<Website> {
@ -8,7 +8,7 @@ export async function loadWebsite(websiteId: string): Promise<Website> {
if (cache.enabled) {
website = await cache.fetchWebsite(websiteId);
} else {
website = await getWebsiteById(websiteId);
website = await getWebsite(websiteId);
}
if (!website || website.deletedAt) {
@ -40,7 +40,7 @@ export async function loadUser(userId: string): Promise<User> {
if (cache.enabled) {
user = await cache.fetchUser(userId);
} else {
user = await getUserById(userId);
user = await getUser(userId);
}
if (!user || user.deletedAt) {

View File

@ -14,7 +14,7 @@ import {
unauthorized,
} from 'next-basics';
import { NextApiRequestCollect } from 'pages/api/send';
import { getUserById } from '../queries';
import { getUser } from '../queries';
const log = debug('umami:middleware');
@ -57,12 +57,12 @@ export const useAuth = createMiddleware(async (req, res, next) => {
const { userId, authKey, grant } = payload || {};
if (userId) {
user = await getUserById(userId);
user = await getUser(userId);
} else if (redis.enabled && authKey) {
const key = await redis.client.get(authKey);
if (key?.userId) {
user = await getUserById(key.userId);
user = await getUser(key.userId);
}
}

View File

@ -175,24 +175,12 @@ async function rawQuery(sql: string, data: object): Promise<any> {
return prisma.rawQuery(query, params);
}
function getPageFilters(filters: SearchFilter): [
{
orderBy: {
[x: string]: string;
}[];
take: number;
skip: number;
},
{
pageSize: number;
page: number;
orderBy: string;
},
] {
async function pagedQuery<T>(model: string, criteria: T, filters: SearchFilter) {
const { page = 1, pageSize = DEFAULT_PAGE_SIZE, orderBy, sortDescending = false } = filters || {};
return [
{
const data = await prisma.client[model].findMany({
...criteria,
...{
...(pageSize > 0 && { take: +pageSize, skip: +pageSize * (page - 1) }),
...(orderBy && {
orderBy: [
@ -202,8 +190,11 @@ function getPageFilters(filters: SearchFilter): [
],
}),
},
{ page: +page, pageSize, orderBy },
];
});
const count = await prisma.client[model].count({ where: (criteria as any).where });
return { data, count, page: +page, pageSize, orderBy };
}
function getQueryMode(): Prisma.QueryMode {
@ -225,7 +216,7 @@ export default {
getTimestampDiffQuery,
getFilterQuery,
parseFilters,
getPageFilters,
getQueryMode,
rawQuery,
pagedQuery,
};

View File

@ -38,6 +38,10 @@ export interface TeamSearchFilter extends SearchFilter {
userId?: string;
}
export interface TeamUserSearchFilter extends SearchFilter {
teamId?: string;
}
export interface ReportSearchFilter extends SearchFilter {
userId?: string;
websiteId?: string;

View File

@ -42,10 +42,7 @@ export default async (
return unauthorized(res);
}
const { page, query, pageSize } = req.query;
const users = await getUsers(
{ page, query, pageSize: +pageSize || undefined },
{
include: {
_count: {
@ -57,6 +54,7 @@ export default async (
},
},
},
req.query,
);
return ok(res, users);

View File

@ -39,25 +39,19 @@ export default async (
return unauthorized(res);
}
const websites = await getWebsites(req.query, {
include: {
teamWebsite: {
include: {
team: {
select: {
name: true,
},
const websites = await getWebsites(
{
include: {
user: {
select: {
username: true,
id: true,
},
},
},
user: {
select: {
username: true,
id: true,
},
},
},
});
req.query,
);
return ok(res, websites);
}

View File

@ -2,9 +2,16 @@ import { NextApiRequestAuth } from 'lib/types';
import { useAuth } from 'lib/middleware';
import { NextApiResponse } from 'next';
import { ok } from 'next-basics';
import { getUserTeams } from 'queries/admin/team';
export default async (req: NextApiRequestAuth, res: NextApiResponse) => {
await useAuth(req, res);
return ok(res, req.auth.user);
const { user } = req.auth;
const teams = await getUserTeams(user.id);
user['teams'] = teams.data.map(n => n);
return ok(res, user);
};

View File

@ -9,7 +9,7 @@ import {
methodNotAllowed,
ok,
} from 'next-basics';
import { getUserById, updateUser } from 'queries';
import { getUser, updateUser } from 'queries';
import * as yup from 'yup';
export interface UserPasswordRequestQuery {
@ -43,7 +43,7 @@ export default async (
const { id } = req.auth.user;
if (req.method === 'POST') {
const user = await getUserById(id, { includePassword: true });
const user = await getUser(id, { includePassword: true });
if (!checkPassword(currentPassword, user.password)) {
return badRequest(res, 'Current password is incorrect');

View File

@ -3,7 +3,7 @@ import { useAuth, useCors, useValidate } from 'lib/middleware';
import { NextApiRequestQueryBody, ReportType, YupRequest } from 'lib/types';
import { NextApiResponse } from 'next';
import { methodNotAllowed, ok, unauthorized } from 'next-basics';
import { deleteReport, getReportById, updateReport } from 'queries';
import { deleteReport, getReport, updateReport } from 'queries';
import * as yup from 'yup';
export interface ReportRequestQuery {
@ -54,7 +54,7 @@ export default async (
} = req.auth;
if (req.method === 'GET') {
const report = await getReportById(reportId);
const report = await getReport(reportId);
if (!(await canViewReport(req.auth, report))) {
return unauthorized(res);
@ -68,7 +68,7 @@ export default async (
if (req.method === 'POST') {
const { websiteId, type, name, description, parameters } = req.body;
const report = await getReportById(reportId);
const report = await getReport(reportId);
if (!(await canUpdateReport(req.auth, report))) {
return unauthorized(res);
@ -87,7 +87,7 @@ export default async (
}
if (req.method === 'DELETE') {
const report = await getReportById(reportId);
const report = await getReport(reportId);
if (!(await canDeleteReport(req.auth, report))) {
return unauthorized(res);

View File

@ -4,7 +4,7 @@ import { NextApiRequestQueryBody, SearchFilter } from 'lib/types';
import { pageInfo } from 'lib/schema';
import { NextApiResponse } from 'next';
import { methodNotAllowed, ok } from 'next-basics';
import { createReport, getReportsByUserId } from 'queries';
import { createReport, getUserReports } from 'queries';
import * as yup from 'yup';
export interface ReportsRequestQuery extends SearchFilter {}
@ -52,11 +52,10 @@ export default async (
if (req.method === 'GET') {
const { page, query, pageSize } = req.query;
const data = await getReportsByUserId(userId, {
const data = await getUserReports(userId, {
page,
pageSize: +pageSize || undefined,
query,
includeTeams: true,
});
return ok(res, data);

View File

@ -3,7 +3,7 @@ import { useValidate } from 'lib/middleware';
import { NextApiRequestQueryBody } from 'lib/types';
import { NextApiResponse } from 'next';
import { createToken, methodNotAllowed, notFound, ok } from 'next-basics';
import { getWebsiteByShareId } from 'queries';
import { getSharedWebsite } from 'queries';
import * as yup from 'yup';
export interface ShareRequestQuery {
@ -30,7 +30,7 @@ export default async (
const { id: shareId } = req.query;
if (req.method === 'GET') {
const website = await getWebsiteByShareId(shareId);
const website = await getSharedWebsite(shareId);
if (website) {
const data = { websiteId: website.id };

View File

@ -4,7 +4,7 @@ import { useAuth, useValidate } from 'lib/middleware';
import { NextApiRequestQueryBody } from 'lib/types';
import { NextApiResponse } from 'next';
import { methodNotAllowed, notFound, ok, unauthorized } from 'next-basics';
import { deleteTeam, getTeamById, updateTeam } from 'queries';
import { deleteTeam, getTeam, updateTeam } from 'queries';
import * as yup from 'yup';
export interface TeamRequestQuery {
@ -44,7 +44,7 @@ export default async (
return unauthorized(res);
}
const team = await getTeamById(teamId, { includeTeamUser: true });
const team = await getTeam(teamId, { includeMembers: true });
if (!team) {
return notFound(res);

View File

@ -4,7 +4,7 @@ import { pageInfo } from 'lib/schema';
import { NextApiRequestQueryBody, SearchFilter } from 'lib/types';
import { NextApiResponse } from 'next';
import { badRequest, methodNotAllowed, ok, unauthorized } from 'next-basics';
import { createTeamUser, getTeamUser, getUsersByTeamId } from 'queries';
import { createTeamUser, getTeamUser, getTeamUsers } from 'queries';
import * as yup from 'yup';
export interface TeamUserRequestQuery extends SearchFilter {
@ -46,7 +46,7 @@ export default async (
const { query, page, pageSize } = req.query;
const users = await getUsersByTeamId(teamId, {
const users = await getTeamUsers(teamId, {
query,
page,
pageSize: +pageSize || undefined,

View File

@ -5,7 +5,7 @@ import { NextApiRequestQueryBody, SearchFilter } from 'lib/types';
import { pageInfo } from 'lib/schema';
import { NextApiResponse } from 'next';
import { methodNotAllowed, ok, unauthorized } from 'next-basics';
import { createWebsite, getWebsitesByTeamId } from 'queries';
import { createWebsite, getTeamWebsites } from 'queries';
import { uuid } from 'lib/crypto';
export interface TeamWebsiteRequestQuery extends SearchFilter {
@ -46,7 +46,7 @@ export default async (
const { page, query, pageSize } = req.query;
const websites = await getWebsitesByTeamId(teamId, {
const websites = await getTeamWebsites(teamId, {
page,
query,
pageSize: +pageSize || undefined,

View File

@ -6,7 +6,7 @@ import { NextApiRequestQueryBody, SearchFilter } from 'lib/types';
import { pageInfo } from 'lib/schema';
import { NextApiResponse } from 'next';
import { getRandomChars, methodNotAllowed, ok, unauthorized } from 'next-basics';
import { createTeam, getTeamsByUserId } from 'queries';
import { createTeam, getUserTeams } from 'queries';
import * as yup from 'yup';
export interface TeamsRequestQuery extends SearchFilter {}
@ -37,7 +37,7 @@ export default async (
if (req.method === 'GET') {
const { page, query, pageSize } = req.query;
const results = await getTeamsByUserId(userId, {
const results = await getUserTeams(userId, {
page,
query,
pageSize: +pageSize || undefined,

View File

@ -4,7 +4,7 @@ import { useAuth, useValidate } from 'lib/middleware';
import { NextApiRequestQueryBody } from 'lib/types';
import { NextApiResponse } from 'next';
import { methodNotAllowed, notFound, ok } from 'next-basics';
import { createTeamUser, getTeamByAccessCode, getTeamUser } from 'queries';
import { createTeamUser, findTeam, getTeamUser } from 'queries';
import * as yup from 'yup';
export interface TeamsJoinRequestBody {
accessCode: string;
@ -26,7 +26,11 @@ export default async (
if (req.method === 'POST') {
const { accessCode } = req.body;
const team = await getTeamByAccessCode(accessCode);
const team = await findTeam({
where: {
accessCode,
},
});
if (!team) {
return notFound(res, 'message.team-not-found');

View File

@ -3,7 +3,7 @@ import { useAuth, useValidate } from 'lib/middleware';
import { NextApiRequestQueryBody, Role, User } from 'lib/types';
import { NextApiResponse } from 'next';
import { badRequest, hashPassword, methodNotAllowed, ok, unauthorized } from 'next-basics';
import { deleteUser, getUserById, getUserByUsername, updateUser } from 'queries';
import { deleteUser, getUser, getUserByUsername, updateUser } from 'queries';
import * as yup from 'yup';
export interface UserRequestQuery {
@ -45,7 +45,7 @@ export default async (
return unauthorized(res);
}
const user = await getUserById(id);
const user = await getUser(id);
return ok(res, user);
}
@ -57,7 +57,7 @@ export default async (
const { username, password, role } = req.body;
const user = await getUserById(id);
const user = await getUser(id);
const data: any = {};

View File

@ -4,7 +4,7 @@ import { NextApiRequestQueryBody, SearchFilter } from 'lib/types';
import { pageInfo } from 'lib/schema';
import { NextApiResponse } from 'next';
import { methodNotAllowed, ok, unauthorized } from 'next-basics';
import { getTeamsByUserId } from 'queries';
import { getUserTeams } from 'queries';
export interface UserTeamsRequestQuery extends SearchFilter {
id: string;
@ -41,7 +41,7 @@ export default async (
const { page, query, pageSize } = req.query;
const teams = await getTeamsByUserId(userId, {
const teams = await getUserTeams(userId, {
query,
page,
pageSize: +pageSize || undefined,

View File

@ -2,7 +2,7 @@ import { useAuth, useCors, useValidate } from 'lib/middleware';
import { NextApiRequestQueryBody } from 'lib/types';
import { NextApiResponse } from 'next';
import { methodNotAllowed, ok, unauthorized } from 'next-basics';
import { getEventDataUsage, getEventUsage, getUserWebsites } from 'queries';
import { getAllWebsites, getEventDataUsage, getEventUsage } from 'queries';
import * as yup from 'yup';
export interface UserUsageRequestQuery {
@ -26,7 +26,7 @@ const schema = {
GET: yup.object().shape({
id: yup.string().uuid().required(),
startAt: yup.number().integer().required(),
endAt: yup.number().integer().moreThan(yup.ref('startAt')).required(),
endAt: yup.number().integer().moreThan(yup.ref<number>('startAt')).required(),
}),
};
@ -50,7 +50,7 @@ export default async (
const startDate = new Date(+startAt);
const endDate = new Date(+endAt);
const websites = await getUserWebsites(userId);
const websites = await getAllWebsites(userId);
const websiteIds = websites.map(a => a.id);

View File

@ -3,7 +3,7 @@ import { NextApiRequestQueryBody, SearchFilter } from 'lib/types';
import { pageInfo } from 'lib/schema';
import { NextApiResponse } from 'next';
import { methodNotAllowed, ok, unauthorized } from 'next-basics';
import { getWebsitesByUserId } from 'queries';
import { getUserWebsites } from 'queries';
import * as yup from 'yup';
export interface UserWebsitesRequestQuery extends SearchFilter {
@ -37,7 +37,7 @@ export default async (
return unauthorized(res);
}
const websites = await getWebsitesByUserId(userId, {
const websites = await getUserWebsites(userId, {
page,
pageSize: +pageSize || undefined,
query,

View File

@ -3,7 +3,7 @@ import { methodNotAllowed, ok, serverError, unauthorized } from 'next-basics';
import { Website, NextApiRequestQueryBody } from 'lib/types';
import { canViewWebsite, canUpdateWebsite, canDeleteWebsite } from 'lib/auth';
import { useAuth, useCors, useValidate } from 'lib/middleware';
import { deleteWebsite, getWebsiteById, updateWebsite } from 'queries';
import { deleteWebsite, getWebsite, updateWebsite } from 'queries';
import { SHARE_ID_REGEX } from 'lib/constants';
export interface WebsiteRequestQuery {
@ -45,7 +45,7 @@ export default async (
return unauthorized(res);
}
const website = await getWebsiteById(websiteId);
const website = await getWebsite(websiteId);
return ok(res, website);
}

View File

@ -4,7 +4,7 @@ import { useAuth, useCors, useValidate } from 'lib/middleware';
import { NextApiRequestQueryBody, SearchFilter } from 'lib/types';
import { NextApiResponse } from 'next';
import { methodNotAllowed, ok, unauthorized } from 'next-basics';
import { getReportsByWebsiteId } from 'queries';
import { getWebsiteReports } from 'queries';
import { pageInfo } from 'lib/schema';
export interface ReportsRequestQuery extends SearchFilter {
@ -35,7 +35,7 @@ export default async (
const { page, query, pageSize } = req.query;
const data = await getReportsByWebsiteId(websiteId, {
const data = await getWebsiteReports(websiteId, {
page,
pageSize: +pageSize || undefined,
query,

View File

@ -1,39 +1,30 @@
import { Prisma, Report } from '@prisma/client';
import prisma from 'lib/prisma';
import { FilterResult, ReportSearchFilter } from 'lib/types';
import ReportFindUniqueArgs = Prisma.ReportFindUniqueArgs;
import ReportFindManyArgs = Prisma.ReportFindManyArgs;
export async function createReport(data: Prisma.ReportUncheckedCreateInput): Promise<Report> {
return prisma.client.report.create({ data });
async function findReport(criteria: ReportFindUniqueArgs) {
return prisma.client.report.findUnique(criteria);
}
export async function getReportById(reportId: string): Promise<Report> {
return prisma.client.report.findUnique({
export async function getReport(reportId: string): Promise<Report> {
return findReport({
where: {
id: reportId,
},
});
}
export async function updateReport(
reportId: string,
data: Prisma.ReportUpdateInput,
): Promise<Report> {
return prisma.client.report.update({ where: { id: reportId }, data });
}
export async function deleteReport(reportId: string): Promise<Report> {
return prisma.client.report.delete({ where: { id: reportId } });
}
export async function getReports(
params: ReportSearchFilter,
options?: { include?: Prisma.ReportInclude },
criteria: ReportFindManyArgs,
filters: ReportSearchFilter = {},
): Promise<FilterResult<Report[]>> {
const { query, userId, websiteId } = params;
const mode = prisma.getQueryMode();
const { query, userId, websiteId } = filters;
const where: Prisma.ReportWhereInput = {
...criteria.where,
userId,
websiteId,
AND: [
@ -93,32 +84,18 @@ export async function getReports(
],
};
const [pageFilters, pageInfo] = prisma.getPageFilters(params);
const reports = await prisma.client.report.findMany({
where,
...pageFilters,
...(options?.include && { include: options.include }),
});
const count = await prisma.client.report.count({
where,
});
return {
data: reports,
count,
...pageInfo,
};
return prisma.pagedQuery('report', { where }, filters);
}
export async function getReportsByUserId(
export async function getUserReports(
userId: string,
filter?: ReportSearchFilter,
filters?: ReportSearchFilter,
): Promise<FilterResult<Report[]>> {
return getReports(
{ userId, ...filter },
{
where: {
userId,
},
include: {
website: {
select: {
@ -128,12 +105,35 @@ export async function getReportsByUserId(
},
},
},
filters,
);
}
export async function getReportsByWebsiteId(
export async function getWebsiteReports(
websiteId: string,
filter: ReportSearchFilter,
filters: ReportSearchFilter = {},
): Promise<FilterResult<Report[]>> {
return getReports({ websiteId, ...filter });
return getReports(
{
where: {
websiteId,
},
},
filters,
);
}
export async function createReport(data: Prisma.ReportUncheckedCreateInput): Promise<Report> {
return prisma.client.report.create({ data });
}
export async function updateReport(
reportId: string,
data: Prisma.ReportUpdateInput,
): Promise<Report> {
return prisma.client.report.update({ where: { id: reportId }, data });
}
export async function deleteReport(reportId: string): Promise<Report> {
return prisma.client.report.delete({ where: { id: reportId } });
}

View File

@ -3,29 +3,109 @@ import { ROLES } from 'lib/constants';
import { uuid } from 'lib/crypto';
import prisma from 'lib/prisma';
import { FilterResult, TeamSearchFilter } from 'lib/types';
import TeamFindManyArgs = Prisma.TeamFindManyArgs;
export interface GetTeamOptions {
includeTeamUser?: boolean;
export async function findTeam(criteria: Prisma.TeamFindFirstArgs): Promise<Team> {
return prisma.client.team.findFirst(criteria);
}
async function getTeam(where: Prisma.TeamWhereInput, options: GetTeamOptions = {}): Promise<Team> {
const { includeTeamUser = false } = options;
const { client } = prisma;
export async function getTeam(teamId: string, options: { includeMembers?: boolean } = {}) {
const { includeMembers } = options;
return client.team.findFirst({
where,
include: {
teamUser: includeTeamUser,
return findTeam({
where: {
id: teamId,
},
...(includeMembers && { include: { teamUser: true } }),
});
}
export function getTeamById(teamId: string, options: GetTeamOptions = {}) {
return getTeam({ id: teamId }, options);
export async function getTeams(
criteria: TeamFindManyArgs,
filters: TeamSearchFilter = {},
): Promise<FilterResult<Team[]>> {
const mode = prisma.getQueryMode();
const { userId, query } = filters;
const where: Prisma.TeamWhereInput = {
...criteria.where,
...(userId && {
teamUser: {
some: { userId },
},
}),
...(query && {
AND: {
OR: [
{
name: {
startsWith: query,
mode,
},
},
{
teamUser: {
some: {
role: ROLES.teamOwner,
user: {
username: {
startsWith: query,
mode,
},
},
},
},
},
],
},
}),
};
return prisma.pagedQuery<TeamFindManyArgs>(
'team',
{
...criteria,
where,
},
filters,
);
}
export function getTeamByAccessCode(accessCode: string, options: GetTeamOptions = {}) {
return getTeam({ accessCode }, options);
export async function getUserTeams(userId: string, filters: TeamSearchFilter = {}) {
return getTeams(
{
where: {
teamUser: {
some: { userId },
},
},
include: {
teamUser: {
include: {
user: {
select: {
id: true,
username: true,
},
},
},
},
_count: {
select: {
website: {
where: { deletedAt: null },
},
teamUser: {
where: {
user: { deletedAt: null },
},
},
},
},
},
},
filters,
);
}
export async function createTeam(data: Prisma.TeamCreateInput, userId: string): Promise<any> {
@ -93,94 +173,3 @@ export async function deleteTeam(
}),
]);
}
export async function getTeams(
filters: TeamSearchFilter,
options?: { include?: Prisma.TeamInclude },
): Promise<FilterResult<Team[]>> {
const { userId, query } = filters;
const mode = prisma.getQueryMode();
const { client } = prisma;
const where: Prisma.TeamWhereInput = {
...(userId && {
teamUser: {
some: { userId },
},
}),
...(query && {
AND: {
OR: [
{
name: { startsWith: query, mode },
},
{
teamUser: {
some: {
role: ROLES.teamOwner,
user: {
username: {
startsWith: query,
mode,
},
},
},
},
},
],
},
}),
};
const [pageFilters, getParameters] = prisma.getPageFilters({
orderBy: 'name',
...filters,
});
const teams = await client.team.findMany({
where: {
...where,
},
...pageFilters,
...(options?.include && { include: options?.include }),
});
const count = await client.team.count({ where });
return { data: teams, count, ...getParameters };
}
export async function getTeamsByUserId(
userId: string,
filter?: TeamSearchFilter,
): Promise<FilterResult<Team[]>> {
return getTeams(
{ userId, ...filter },
{
include: {
teamUser: {
include: {
user: {
select: {
id: true,
username: true,
},
},
},
},
_count: {
select: {
website: {
where: { deletedAt: null },
},
teamUser: {
where: {
user: { deletedAt: null },
},
},
},
},
},
},
);
}

View File

@ -1,14 +1,7 @@
import { Prisma, TeamUser } from '@prisma/client';
import { TeamUser } from '@prisma/client';
import { uuid } from 'lib/crypto';
import prisma from 'lib/prisma';
export async function getTeamUserById(teamUserId: string): Promise<TeamUser> {
return prisma.client.teamUser.findUnique({
where: {
id: teamUserId,
},
});
}
import { FilterResult, TeamUserSearchFilter } from 'lib/types';
export async function getTeamUser(teamId: string, userId: string): Promise<TeamUser> {
return prisma.client.teamUser.findFirst({
@ -21,20 +14,25 @@ export async function getTeamUser(teamId: string, userId: string): Promise<TeamU
export async function getTeamUsers(
teamId: string,
): Promise<(TeamUser & { user: { id: string; username: string } })[]> {
return prisma.client.teamUser.findMany({
where: {
teamId,
},
include: {
user: {
select: {
id: true,
username: true,
filters?: TeamUserSearchFilter,
): Promise<FilterResult<TeamUser[]>> {
return prisma.pagedQuery(
'teamUser',
{
where: {
teamId,
},
include: {
user: {
select: {
id: true,
username: true,
},
},
},
},
});
filters,
);
}
export async function createTeamUser(
@ -52,18 +50,6 @@ export async function createTeamUser(
});
}
export async function updateTeamUser(
teamUserId: string,
data: Prisma.TeamUserUpdateInput,
): Promise<TeamUser> {
return prisma.client.teamUser.update({
where: {
id: teamUserId,
},
data,
});
}
export async function deleteTeamUser(teamId: string, userId: string): Promise<TeamUser> {
const { client } = prisma;
@ -74,15 +60,3 @@ export async function deleteTeamUser(teamId: string, userId: string): Promise<Te
},
});
}
export async function deleteTeamUserByUserId(
userId: string,
teamId: string,
): Promise<Prisma.BatchPayload> {
return prisma.client.teamUser.deleteMany({
where: {
userId,
teamId,
},
});
}

View File

@ -4,24 +4,25 @@ import { ROLES } from 'lib/constants';
import prisma from 'lib/prisma';
import { FilterResult, Role, User, UserSearchFilter } from 'lib/types';
import { getRandomChars } from 'next-basics';
import UserFindManyArgs = Prisma.UserFindManyArgs;
export interface GetUserOptions {
includePassword?: boolean;
showDeleted?: boolean;
}
async function getUser(
where: Prisma.UserWhereUniqueInput,
async function findUser(
criteria: Prisma.UserFindUniqueArgs,
options: GetUserOptions = {},
): Promise<User> {
const { includePassword = false, showDeleted = false } = options;
if (showDeleted) {
where.deletedAt = null;
}
return prisma.client.user.findUnique({
where,
...criteria,
where: {
...criteria.where,
...(showDeleted && { delatedAt: null }),
},
select: {
id: true,
username: true,
@ -32,19 +33,26 @@ async function getUser(
});
}
export async function getUserById(id: string, options: GetUserOptions = {}) {
return getUser({ id }, options);
export async function getUser(userId: string, options: GetUserOptions = {}) {
return findUser(
{
where: {
id: userId,
},
},
options,
);
}
export async function getUserByUsername(username: string, options: GetUserOptions = {}) {
return getUser({ username }, options);
return findUser({ where: { username } }, options);
}
export async function getUsers(
params: UserSearchFilter,
options?: { include?: Prisma.UserInclude },
criteria: UserFindManyArgs,
filters?: UserSearchFilter,
): Promise<FilterResult<User[]>> {
const { teamId, query } = params;
const { teamId, query } = filters;
const mode = prisma.getQueryMode();
const where: Prisma.UserWhereInput = {
@ -67,49 +75,19 @@ export async function getUsers(
],
},
}),
deletedAt: null,
};
const [pageFilters, getParameters] = prisma.getPageFilters({
orderBy: 'createdAt',
sortDescending: true,
...params,
});
const users = await prisma.client.user
.findMany({
where: {
...where,
deletedAt: null,
},
...pageFilters,
...(options?.include && { include: options.include }),
})
.then((a: { [x: string]: any; password: any }[]) => {
return a.map(({ password, ...rest }) => rest);
});
const count = await prisma.client.user.count({
where: {
...where,
deletedAt: null,
},
});
return { data: users as any, count, ...getParameters };
}
export async function getUsersByTeamId(teamId: string, filter?: UserSearchFilter) {
return getUsers(
{ teamId, ...filter },
return prisma.pagedQuery(
'user',
{
include: {
teamUser: {
select: {
teamId: true,
role: true,
},
},
},
...criteria,
where,
},
{
orderBy: 'createdAt',
sortDescending: true,
...filters,
},
);
}

View File

@ -2,29 +2,37 @@ import { Prisma, Website } from '@prisma/client';
import cache from 'lib/cache';
import prisma from 'lib/prisma';
import { FilterResult, WebsiteSearchFilter } from 'lib/types';
import WebsiteFindManyArgs = Prisma.WebsiteFindManyArgs;
async function getWebsite(where: Prisma.WebsiteWhereUniqueInput): Promise<Website> {
return prisma.client.website.findUnique({
where,
async function findWebsite(criteria: Prisma.WebsiteFindManyArgs): Promise<Website> {
return prisma.client.website.findUnique(criteria);
}
export async function getWebsite(websiteId: string) {
return findWebsite({
where: {
id: websiteId,
},
});
}
export async function getWebsiteById(id: string) {
return getWebsite({ id });
}
export async function getWebsiteByShareId(shareId: string) {
return getWebsite({ shareId });
export async function getSharedWebsite(shareId: string) {
return findWebsite({
where: {
shareId,
},
});
}
export async function getWebsites(
criteria: WebsiteFindManyArgs,
filters: WebsiteSearchFilter,
options?: { include?: Prisma.WebsiteInclude },
): Promise<FilterResult<Website[]>> {
const { userId, teamId, query } = filters;
const mode = prisma.getQueryMode();
const where: Prisma.WebsiteWhereInput = {
...criteria.where,
AND: [
{
OR: [
@ -47,34 +55,29 @@ export async function getWebsites(
: [],
},
],
deletedAt: null,
};
const [pageFilters, getParameters] = prisma.getPageFilters({
orderBy: 'name',
...filters,
});
const websites = await prisma.client.website.findMany({
where: {
...where,
deletedAt: null,
},
...pageFilters,
...(options?.include && { include: options.include }),
});
const count = await prisma.client.website.count({ where: { ...where, deletedAt: null } });
return { data: websites, count, ...getParameters };
return prisma.pagedQuery('website', { where }, filters);
}
export async function getWebsitesByUserId(
export async function getAllWebsites(userId: string) {
return prisma.client.website.findMany({
where: {
userId,
},
});
}
export async function getUserWebsites(
userId: string,
filters?: WebsiteSearchFilter,
): Promise<FilterResult<Website[]>> {
return getWebsites(
{ userId, ...filters },
{
where: {
userId,
},
include: {
user: {
select: {
@ -84,19 +87,22 @@ export async function getWebsitesByUserId(
},
},
},
{
orderBy: 'name',
...filters,
},
);
}
export async function getWebsitesByTeamId(
export async function getTeamWebsites(
teamId: string,
filters?: WebsiteSearchFilter,
): Promise<FilterResult<Website[]>> {
return getWebsites(
{
teamId,
...filters,
},
{
where: {
teamId,
},
include: {
user: {
select: {
@ -106,80 +112,10 @@ export async function getWebsitesByTeamId(
},
},
},
filters,
);
}
export async function getUserWebsites(
userId: string,
options?: { includeTeams: boolean },
): Promise<Website[]> {
const { rawQuery } = prisma;
if (options?.includeTeams) {
const websites = await rawQuery(
`
select
website_id as "id",
name,
domain,
share_id as "shareId",
reset_at as "resetAt",
user_id as "userId",
created_at as "createdAt",
updated_at as "updatedAt",
deleted_at as "deletedAt",
null as "teamId",
null as "teamName"
from website
where user_id = {{userId::uuid}}
and deleted_at is null
union
select
w.website_id as "id",
w.name,
w.domain,
w.share_id as "shareId",
w.reset_at as "resetAt",
w.user_id as "userId",
w.created_at as "createdAt",
w.updated_at as "updatedAt",
w.deleted_at as "deletedAt",
t.team_id as "teamId",
t.name as "teamName"
from website w
inner join team_website tw
on tw.website_id = w.website_id
inner join team t
on t.team_id = tw.team_id
inner join team_user tu
on tu.team_id = tw.team_id
where tu.user_id = {{userId::uuid}}
and w.deleted_at is null
`,
{ userId },
);
return websites.reduce((arr, item) => {
if (!arr.find(({ id }) => id === item.id)) {
return arr.concat(item);
}
return arr;
}, []);
}
return prisma.client.website.findMany({
where: {
userId,
deletedAt: null,
},
orderBy: [
{
name: 'asc',
},
],
});
}
export async function createWebsite(
data: Prisma.WebsiteCreateInput | Prisma.WebsiteUncheckedCreateInput,
): Promise<Website> {