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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,24 +1,13 @@
import { useApi, useFilterQuery } from 'components/hooks';
import DataTable from 'components/common/DataTable'; import DataTable from 'components/common/DataTable';
import useCache from 'store/cache';
import TeamMembersTable from './TeamMembersTable'; import TeamMembersTable from './TeamMembersTable';
import useTeamMembers from 'components/hooks/queries/useTeamMembers';
export function TeamMembers({ teamId, readOnly }: { teamId: string; readOnly: boolean }) { export function TeamMembers({ teamId, allowEdit }: { teamId: string; allowEdit: boolean }) {
const { get } = useApi(); const queryResult = useTeamMembers(teamId);
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,
});
return ( return (
<DataTable queryResult={queryResult}> <DataTable queryResult={queryResult}>
{({ data }) => <TeamMembersTable data={data} teamId={teamId} readOnly={readOnly} />} {({ data }) => <TeamMembersTable data={data} teamId={teamId} allowEdit={allowEdit} />}
</DataTable> </DataTable>
); );
} }

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -14,7 +14,7 @@ import {
unauthorized, unauthorized,
} from 'next-basics'; } from 'next-basics';
import { NextApiRequestCollect } from 'pages/api/send'; import { NextApiRequestCollect } from 'pages/api/send';
import { getUserById } from '../queries'; import { getUser } from '../queries';
const log = debug('umami:middleware'); const log = debug('umami:middleware');
@ -57,12 +57,12 @@ export const useAuth = createMiddleware(async (req, res, next) => {
const { userId, authKey, grant } = payload || {}; const { userId, authKey, grant } = payload || {};
if (userId) { if (userId) {
user = await getUserById(userId); user = await getUser(userId);
} else if (redis.enabled && authKey) { } else if (redis.enabled && authKey) {
const key = await redis.client.get(authKey); const key = await redis.client.get(authKey);
if (key?.userId) { 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); return prisma.rawQuery(query, params);
} }
function getPageFilters(filters: SearchFilter): [ async function pagedQuery<T>(model: string, criteria: T, filters: SearchFilter) {
{
orderBy: {
[x: string]: string;
}[];
take: number;
skip: number;
},
{
pageSize: number;
page: number;
orderBy: string;
},
] {
const { page = 1, pageSize = DEFAULT_PAGE_SIZE, orderBy, sortDescending = false } = filters || {}; 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) }), ...(pageSize > 0 && { take: +pageSize, skip: +pageSize * (page - 1) }),
...(orderBy && { ...(orderBy && {
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 { function getQueryMode(): Prisma.QueryMode {
@ -225,7 +216,7 @@ export default {
getTimestampDiffQuery, getTimestampDiffQuery,
getFilterQuery, getFilterQuery,
parseFilters, parseFilters,
getPageFilters,
getQueryMode, getQueryMode,
rawQuery, rawQuery,
pagedQuery,
}; };

View File

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

View File

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

View File

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

View File

@ -2,9 +2,16 @@ import { NextApiRequestAuth } from 'lib/types';
import { useAuth } from 'lib/middleware'; import { useAuth } from 'lib/middleware';
import { NextApiResponse } from 'next'; import { NextApiResponse } from 'next';
import { ok } from 'next-basics'; import { ok } from 'next-basics';
import { getUserTeams } from 'queries/admin/team';
export default async (req: NextApiRequestAuth, res: NextApiResponse) => { export default async (req: NextApiRequestAuth, res: NextApiResponse) => {
await useAuth(req, res); 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, methodNotAllowed,
ok, ok,
} from 'next-basics'; } from 'next-basics';
import { getUserById, updateUser } from 'queries'; import { getUser, updateUser } from 'queries';
import * as yup from 'yup'; import * as yup from 'yup';
export interface UserPasswordRequestQuery { export interface UserPasswordRequestQuery {
@ -43,7 +43,7 @@ export default async (
const { id } = req.auth.user; const { id } = req.auth.user;
if (req.method === 'POST') { if (req.method === 'POST') {
const user = await getUserById(id, { includePassword: true }); const user = await getUser(id, { includePassword: true });
if (!checkPassword(currentPassword, user.password)) { if (!checkPassword(currentPassword, user.password)) {
return badRequest(res, 'Current password is incorrect'); 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 { NextApiRequestQueryBody, ReportType, YupRequest } from 'lib/types';
import { NextApiResponse } from 'next'; import { NextApiResponse } from 'next';
import { methodNotAllowed, ok, unauthorized } from 'next-basics'; import { methodNotAllowed, ok, unauthorized } from 'next-basics';
import { deleteReport, getReportById, updateReport } from 'queries'; import { deleteReport, getReport, updateReport } from 'queries';
import * as yup from 'yup'; import * as yup from 'yup';
export interface ReportRequestQuery { export interface ReportRequestQuery {
@ -54,7 +54,7 @@ export default async (
} = req.auth; } = req.auth;
if (req.method === 'GET') { if (req.method === 'GET') {
const report = await getReportById(reportId); const report = await getReport(reportId);
if (!(await canViewReport(req.auth, report))) { if (!(await canViewReport(req.auth, report))) {
return unauthorized(res); return unauthorized(res);
@ -68,7 +68,7 @@ export default async (
if (req.method === 'POST') { if (req.method === 'POST') {
const { websiteId, type, name, description, parameters } = req.body; const { websiteId, type, name, description, parameters } = req.body;
const report = await getReportById(reportId); const report = await getReport(reportId);
if (!(await canUpdateReport(req.auth, report))) { if (!(await canUpdateReport(req.auth, report))) {
return unauthorized(res); return unauthorized(res);
@ -87,7 +87,7 @@ export default async (
} }
if (req.method === 'DELETE') { if (req.method === 'DELETE') {
const report = await getReportById(reportId); const report = await getReport(reportId);
if (!(await canDeleteReport(req.auth, report))) { if (!(await canDeleteReport(req.auth, report))) {
return unauthorized(res); return unauthorized(res);

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -4,7 +4,7 @@ import { useAuth, useValidate } from 'lib/middleware';
import { NextApiRequestQueryBody } from 'lib/types'; import { NextApiRequestQueryBody } from 'lib/types';
import { NextApiResponse } from 'next'; import { NextApiResponse } from 'next';
import { methodNotAllowed, notFound, ok } from 'next-basics'; import { methodNotAllowed, notFound, ok } from 'next-basics';
import { createTeamUser, getTeamByAccessCode, getTeamUser } from 'queries'; import { createTeamUser, findTeam, getTeamUser } from 'queries';
import * as yup from 'yup'; import * as yup from 'yup';
export interface TeamsJoinRequestBody { export interface TeamsJoinRequestBody {
accessCode: string; accessCode: string;
@ -26,7 +26,11 @@ export default async (
if (req.method === 'POST') { if (req.method === 'POST') {
const { accessCode } = req.body; const { accessCode } = req.body;
const team = await getTeamByAccessCode(accessCode); const team = await findTeam({
where: {
accessCode,
},
});
if (!team) { if (!team) {
return notFound(res, 'message.team-not-found'); 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 { NextApiRequestQueryBody, Role, User } from 'lib/types';
import { NextApiResponse } from 'next'; import { NextApiResponse } from 'next';
import { badRequest, hashPassword, methodNotAllowed, ok, unauthorized } from 'next-basics'; 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'; import * as yup from 'yup';
export interface UserRequestQuery { export interface UserRequestQuery {
@ -45,7 +45,7 @@ export default async (
return unauthorized(res); return unauthorized(res);
} }
const user = await getUserById(id); const user = await getUser(id);
return ok(res, user); return ok(res, user);
} }
@ -57,7 +57,7 @@ export default async (
const { username, password, role } = req.body; const { username, password, role } = req.body;
const user = await getUserById(id); const user = await getUser(id);
const data: any = {}; const data: any = {};

View File

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

View File

@ -2,7 +2,7 @@ import { useAuth, useCors, useValidate } from 'lib/middleware';
import { NextApiRequestQueryBody } from 'lib/types'; import { NextApiRequestQueryBody } from 'lib/types';
import { NextApiResponse } from 'next'; import { NextApiResponse } from 'next';
import { methodNotAllowed, ok, unauthorized } from 'next-basics'; import { methodNotAllowed, ok, unauthorized } from 'next-basics';
import { getEventDataUsage, getEventUsage, getUserWebsites } from 'queries'; import { getAllWebsites, getEventDataUsage, getEventUsage } from 'queries';
import * as yup from 'yup'; import * as yup from 'yup';
export interface UserUsageRequestQuery { export interface UserUsageRequestQuery {
@ -26,7 +26,7 @@ const schema = {
GET: yup.object().shape({ GET: yup.object().shape({
id: yup.string().uuid().required(), id: yup.string().uuid().required(),
startAt: yup.number().integer().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 startDate = new Date(+startAt);
const endDate = new Date(+endAt); const endDate = new Date(+endAt);
const websites = await getUserWebsites(userId); const websites = await getAllWebsites(userId);
const websiteIds = websites.map(a => a.id); 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 { pageInfo } from 'lib/schema';
import { NextApiResponse } from 'next'; import { NextApiResponse } from 'next';
import { methodNotAllowed, ok, unauthorized } from 'next-basics'; import { methodNotAllowed, ok, unauthorized } from 'next-basics';
import { getWebsitesByUserId } from 'queries'; import { getUserWebsites } from 'queries';
import * as yup from 'yup'; import * as yup from 'yup';
export interface UserWebsitesRequestQuery extends SearchFilter { export interface UserWebsitesRequestQuery extends SearchFilter {
@ -37,7 +37,7 @@ export default async (
return unauthorized(res); return unauthorized(res);
} }
const websites = await getWebsitesByUserId(userId, { const websites = await getUserWebsites(userId, {
page, page,
pageSize: +pageSize || undefined, pageSize: +pageSize || undefined,
query, query,

View File

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

View File

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

View File

@ -1,39 +1,30 @@
import { Prisma, Report } from '@prisma/client'; import { Prisma, Report } from '@prisma/client';
import prisma from 'lib/prisma'; import prisma from 'lib/prisma';
import { FilterResult, ReportSearchFilter } from 'lib/types'; import { FilterResult, ReportSearchFilter } from 'lib/types';
import ReportFindUniqueArgs = Prisma.ReportFindUniqueArgs;
import ReportFindManyArgs = Prisma.ReportFindManyArgs;
export async function createReport(data: Prisma.ReportUncheckedCreateInput): Promise<Report> { async function findReport(criteria: ReportFindUniqueArgs) {
return prisma.client.report.create({ data }); return prisma.client.report.findUnique(criteria);
} }
export async function getReportById(reportId: string): Promise<Report> { export async function getReport(reportId: string): Promise<Report> {
return prisma.client.report.findUnique({ return findReport({
where: { where: {
id: reportId, 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( export async function getReports(
params: ReportSearchFilter, criteria: ReportFindManyArgs,
options?: { include?: Prisma.ReportInclude }, filters: ReportSearchFilter = {},
): Promise<FilterResult<Report[]>> { ): Promise<FilterResult<Report[]>> {
const { query, userId, websiteId } = params;
const mode = prisma.getQueryMode(); const mode = prisma.getQueryMode();
const { query, userId, websiteId } = filters;
const where: Prisma.ReportWhereInput = { const where: Prisma.ReportWhereInput = {
...criteria.where,
userId, userId,
websiteId, websiteId,
AND: [ AND: [
@ -93,32 +84,18 @@ export async function getReports(
], ],
}; };
const [pageFilters, pageInfo] = prisma.getPageFilters(params); return prisma.pagedQuery('report', { where }, filters);
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,
};
} }
export async function getReportsByUserId( export async function getUserReports(
userId: string, userId: string,
filter?: ReportSearchFilter, filters?: ReportSearchFilter,
): Promise<FilterResult<Report[]>> { ): Promise<FilterResult<Report[]>> {
return getReports( return getReports(
{ userId, ...filter },
{ {
where: {
userId,
},
include: { include: {
website: { website: {
select: { select: {
@ -128,12 +105,35 @@ export async function getReportsByUserId(
}, },
}, },
}, },
filters,
); );
} }
export async function getReportsByWebsiteId( export async function getWebsiteReports(
websiteId: string, websiteId: string,
filter: ReportSearchFilter, filters: ReportSearchFilter = {},
): Promise<FilterResult<Report[]>> { ): 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 { uuid } from 'lib/crypto';
import prisma from 'lib/prisma'; import prisma from 'lib/prisma';
import { FilterResult, TeamSearchFilter } from 'lib/types'; import { FilterResult, TeamSearchFilter } from 'lib/types';
import TeamFindManyArgs = Prisma.TeamFindManyArgs;
export interface GetTeamOptions { export async function findTeam(criteria: Prisma.TeamFindFirstArgs): Promise<Team> {
includeTeamUser?: boolean; return prisma.client.team.findFirst(criteria);
} }
async function getTeam(where: Prisma.TeamWhereInput, options: GetTeamOptions = {}): Promise<Team> { export async function getTeam(teamId: string, options: { includeMembers?: boolean } = {}) {
const { includeTeamUser = false } = options; const { includeMembers } = options;
const { client } = prisma;
return client.team.findFirst({ return findTeam({
where, where: {
include: { id: teamId,
teamUser: includeTeamUser,
}, },
...(includeMembers && { include: { teamUser: true } }),
}); });
} }
export function getTeamById(teamId: string, options: GetTeamOptions = {}) { export async function getTeams(
return getTeam({ id: teamId }, options); 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 = {}) { export async function getUserTeams(userId: string, filters: TeamSearchFilter = {}) {
return getTeam({ accessCode }, options); 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> { 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 { uuid } from 'lib/crypto';
import prisma from 'lib/prisma'; import prisma from 'lib/prisma';
import { FilterResult, TeamUserSearchFilter } from 'lib/types';
export async function getTeamUserById(teamUserId: string): Promise<TeamUser> {
return prisma.client.teamUser.findUnique({
where: {
id: teamUserId,
},
});
}
export async function getTeamUser(teamId: string, userId: string): Promise<TeamUser> { export async function getTeamUser(teamId: string, userId: string): Promise<TeamUser> {
return prisma.client.teamUser.findFirst({ return prisma.client.teamUser.findFirst({
@ -21,20 +14,25 @@ export async function getTeamUser(teamId: string, userId: string): Promise<TeamU
export async function getTeamUsers( export async function getTeamUsers(
teamId: string, teamId: string,
): Promise<(TeamUser & { user: { id: string; username: string } })[]> { filters?: TeamUserSearchFilter,
return prisma.client.teamUser.findMany({ ): Promise<FilterResult<TeamUser[]>> {
where: { return prisma.pagedQuery(
teamId, 'teamUser',
}, {
include: { where: {
user: { teamId,
select: { },
id: true, include: {
username: true, user: {
select: {
id: true,
username: true,
},
}, },
}, },
}, },
}); filters,
);
} }
export async function createTeamUser( 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> { export async function deleteTeamUser(teamId: string, userId: string): Promise<TeamUser> {
const { client } = prisma; 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 prisma from 'lib/prisma';
import { FilterResult, Role, User, UserSearchFilter } from 'lib/types'; import { FilterResult, Role, User, UserSearchFilter } from 'lib/types';
import { getRandomChars } from 'next-basics'; import { getRandomChars } from 'next-basics';
import UserFindManyArgs = Prisma.UserFindManyArgs;
export interface GetUserOptions { export interface GetUserOptions {
includePassword?: boolean; includePassword?: boolean;
showDeleted?: boolean; showDeleted?: boolean;
} }
async function getUser( async function findUser(
where: Prisma.UserWhereUniqueInput, criteria: Prisma.UserFindUniqueArgs,
options: GetUserOptions = {}, options: GetUserOptions = {},
): Promise<User> { ): Promise<User> {
const { includePassword = false, showDeleted = false } = options; const { includePassword = false, showDeleted = false } = options;
if (showDeleted) {
where.deletedAt = null;
}
return prisma.client.user.findUnique({ return prisma.client.user.findUnique({
where, ...criteria,
where: {
...criteria.where,
...(showDeleted && { delatedAt: null }),
},
select: { select: {
id: true, id: true,
username: true, username: true,
@ -32,19 +33,26 @@ async function getUser(
}); });
} }
export async function getUserById(id: string, options: GetUserOptions = {}) { export async function getUser(userId: string, options: GetUserOptions = {}) {
return getUser({ id }, options); return findUser(
{
where: {
id: userId,
},
},
options,
);
} }
export async function getUserByUsername(username: string, options: GetUserOptions = {}) { export async function getUserByUsername(username: string, options: GetUserOptions = {}) {
return getUser({ username }, options); return findUser({ where: { username } }, options);
} }
export async function getUsers( export async function getUsers(
params: UserSearchFilter, criteria: UserFindManyArgs,
options?: { include?: Prisma.UserInclude }, filters?: UserSearchFilter,
): Promise<FilterResult<User[]>> { ): Promise<FilterResult<User[]>> {
const { teamId, query } = params; const { teamId, query } = filters;
const mode = prisma.getQueryMode(); const mode = prisma.getQueryMode();
const where: Prisma.UserWhereInput = { const where: Prisma.UserWhereInput = {
@ -67,49 +75,19 @@ export async function getUsers(
], ],
}, },
}), }),
deletedAt: null,
}; };
const [pageFilters, getParameters] = prisma.getPageFilters({ return prisma.pagedQuery(
orderBy: 'createdAt', 'user',
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 },
{ {
include: { ...criteria,
teamUser: { where,
select: { },
teamId: true, {
role: true, orderBy: 'createdAt',
}, sortDescending: true,
}, ...filters,
},
}, },
); );
} }

View File

@ -2,29 +2,37 @@ import { Prisma, Website } from '@prisma/client';
import cache from 'lib/cache'; import cache from 'lib/cache';
import prisma from 'lib/prisma'; import prisma from 'lib/prisma';
import { FilterResult, WebsiteSearchFilter } from 'lib/types'; import { FilterResult, WebsiteSearchFilter } from 'lib/types';
import WebsiteFindManyArgs = Prisma.WebsiteFindManyArgs;
async function getWebsite(where: Prisma.WebsiteWhereUniqueInput): Promise<Website> { async function findWebsite(criteria: Prisma.WebsiteFindManyArgs): Promise<Website> {
return prisma.client.website.findUnique({ return prisma.client.website.findUnique(criteria);
where, }
export async function getWebsite(websiteId: string) {
return findWebsite({
where: {
id: websiteId,
},
}); });
} }
export async function getWebsiteById(id: string) { export async function getSharedWebsite(shareId: string) {
return getWebsite({ id }); return findWebsite({
} where: {
shareId,
export async function getWebsiteByShareId(shareId: string) { },
return getWebsite({ shareId }); });
} }
export async function getWebsites( export async function getWebsites(
criteria: WebsiteFindManyArgs,
filters: WebsiteSearchFilter, filters: WebsiteSearchFilter,
options?: { include?: Prisma.WebsiteInclude },
): Promise<FilterResult<Website[]>> { ): Promise<FilterResult<Website[]>> {
const { userId, teamId, query } = filters; const { userId, teamId, query } = filters;
const mode = prisma.getQueryMode(); const mode = prisma.getQueryMode();
const where: Prisma.WebsiteWhereInput = { const where: Prisma.WebsiteWhereInput = {
...criteria.where,
AND: [ AND: [
{ {
OR: [ OR: [
@ -47,34 +55,29 @@ export async function getWebsites(
: [], : [],
}, },
], ],
deletedAt: null,
}; };
const [pageFilters, getParameters] = prisma.getPageFilters({ return prisma.pagedQuery('website', { where }, filters);
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 };
} }
export async function getWebsitesByUserId( export async function getAllWebsites(userId: string) {
return prisma.client.website.findMany({
where: {
userId,
},
});
}
export async function getUserWebsites(
userId: string, userId: string,
filters?: WebsiteSearchFilter, filters?: WebsiteSearchFilter,
): Promise<FilterResult<Website[]>> { ): Promise<FilterResult<Website[]>> {
return getWebsites( return getWebsites(
{ userId, ...filters },
{ {
where: {
userId,
},
include: { include: {
user: { user: {
select: { select: {
@ -84,19 +87,22 @@ export async function getWebsitesByUserId(
}, },
}, },
}, },
{
orderBy: 'name',
...filters,
},
); );
} }
export async function getWebsitesByTeamId( export async function getTeamWebsites(
teamId: string, teamId: string,
filters?: WebsiteSearchFilter, filters?: WebsiteSearchFilter,
): Promise<FilterResult<Website[]>> { ): Promise<FilterResult<Website[]>> {
return getWebsites( return getWebsites(
{ {
teamId, where: {
...filters, teamId,
}, },
{
include: { include: {
user: { user: {
select: { 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( export async function createWebsite(
data: Prisma.WebsiteCreateInput | Prisma.WebsiteUncheckedCreateInput, data: Prisma.WebsiteCreateInput | Prisma.WebsiteUncheckedCreateInput,
): Promise<Website> { ): Promise<Website> {