From dcf8b2edaa5c24ce5b0075e51c8e8aaf7ba2a482 Mon Sep 17 00:00:00 2001 From: Brian Cao Date: Thu, 10 Aug 2023 13:26:33 -0700 Subject: [PATCH] Add Search Api/Components. --- .eslintrc.json | 3 +- components/common/Pager.js | 37 ++++ components/common/Pager.module.css | 7 + components/common/SettingsTable.js | 121 ++++++++--- components/input/WebsiteSelect.js | 4 +- components/pages/dashboard/Dashboard.js | 13 +- components/pages/reports/ReportsTable.js | 20 +- .../settings/teams/TeamAddWebsiteForm.js | 32 +-- .../pages/settings/teams/TeamMembers.js | 25 ++- .../pages/settings/teams/TeamMembersTable.js | 26 ++- .../pages/settings/teams/TeamWebsites.js | 25 ++- .../pages/settings/teams/TeamWebsitesTable.js | 26 ++- components/pages/settings/teams/TeamsList.js | 50 +++-- components/pages/settings/teams/TeamsTable.js | 31 ++- components/pages/settings/users/UsersList.js | 32 ++- components/pages/settings/users/UsersTable.js | 21 +- .../pages/settings/websites/WebsitesList.js | 22 +- .../pages/settings/websites/WebsitesTable.js | 19 +- .../pages/websites/WebsiteReportsPage.js | 20 +- hooks/useApiFilter.ts | 28 +++ hooks/useReports.js | 21 +- lib/constants.ts | 16 ++ lib/prisma.ts | 34 ++- lib/types.ts | 55 ++++- package.json | 2 +- pages/api/reports/index.ts | 20 +- pages/api/teams/[id]/users/index.ts | 14 +- pages/api/teams/[id]/websites/index.ts | 15 +- pages/api/teams/index.ts | 15 +- pages/api/users/[id]/websites.ts | 18 +- pages/api/users/index.ts | 9 +- pages/api/websites/index.ts | 7 +- queries/admin/report.ts | 118 +++++++++-- queries/admin/team.ts | 88 +++++++- queries/admin/user.ts | 156 ++++---------- queries/admin/website.ts | 198 +++++++++++++++++- yarn.lock | 8 +- 37 files changed, 1069 insertions(+), 287 deletions(-) create mode 100644 components/common/Pager.js create mode 100644 components/common/Pager.module.css create mode 100644 hooks/useApiFilter.ts diff --git a/.eslintrc.json b/.eslintrc.json index 7a824ff6..25e83d5a 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -50,7 +50,8 @@ "@next/next/no-img-element": "off", "@typescript-eslint/no-empty-function": "off", "@typescript-eslint/no-explicit-any": "off", - "@typescript-eslint/no-var-requires": "off" + "@typescript-eslint/no-var-requires": "off", + "@typescript-eslint/no-empty-interface": "off" }, "globals": { "React": "writable" diff --git a/components/common/Pager.js b/components/common/Pager.js new file mode 100644 index 00000000..584e0669 --- /dev/null +++ b/components/common/Pager.js @@ -0,0 +1,37 @@ +import styles from './Pager.module.css'; +import { Button, Flexbox, Icon, Icons } from 'react-basics'; + +export function Pager({ page, pageSize, count, onPageChange, onPageSizeChange }) { + const maxPage = Math.ceil(count / pageSize); + const lastPage = page === maxPage; + const firstPage = page === 1; + + if (count === 0) { + return null; + } + + const handlePageChange = value => { + const nextPage = page + value; + if (nextPage > 0 && nextPage <= maxPage) { + onPageChange(nextPage); + } + }; + + return ( + + + {`Page ${page} of ${maxPage}`} + + + ); +} + +export default Pager; diff --git a/components/common/Pager.module.css b/components/common/Pager.module.css new file mode 100644 index 00000000..b4ee9f0e --- /dev/null +++ b/components/common/Pager.module.css @@ -0,0 +1,7 @@ +.container { + margin-top: 20px; +} + +.text { + margin: 0 10px; +} diff --git a/components/common/SettingsTable.js b/components/common/SettingsTable.js index 8f039858..9fb4c2a9 100644 --- a/components/common/SettingsTable.js +++ b/components/common/SettingsTable.js @@ -1,37 +1,98 @@ -import { Table, TableHeader, TableBody, TableRow, TableCell, TableColumn } from 'react-basics'; +import EmptyPlaceholder from 'components/common/EmptyPlaceholder'; +import useMessages from 'hooks/useMessages'; +import { useState } from 'react'; +import { + SearchField, + Table, + TableBody, + TableCell, + TableColumn, + TableHeader, + TableRow, +} from 'react-basics'; import styles from './SettingsTable.module.css'; +import Pager from 'components/common/Pager'; + +export function SettingsTable({ + columns = [], + data, + children, + cellRender, + showSearch, + showPaging, + onFilterChange, + onPageChange, + onPageSizeChange, + filterValue, +}) { + const { formatMessage, messages } = useMessages(); + const [filter, setFilter] = useState(filterValue); + const { data: value, page, count, pageSize } = data; + + const handleFilterChange = value => { + setFilter(value); + onFilterChange(value); + }; -export function SettingsTable({ columns = [], data = [], children, cellRender }) { return ( - - - {(column, index) => { - return ( - - {column.label} - - ); - }} - - - {(row, keys, rowIndex) => { - row.action = children(row, keys, rowIndex); + <> + {showSearch && ( + + )} + {value.length === 0 && filterValue && ( + + )} + {value.length > 0 && ( +
+ + {(column, index) => { + return ( + + {column.label} + + ); + }} + + + {(row, keys, rowIndex) => { + row.action = children(row, keys, rowIndex); - return ( - - {(data, key, colIndex) => { - return ( - - - {cellRender ? cellRender(row, data, key, colIndex) : data[key]} - - ); - }} - - ); - }} - -
+ return ( + + {(data, key, colIndex) => { + return ( + + + {cellRender ? cellRender(row, data, key, colIndex) : data[key]} + + ); + }} + + ); + }} + + {showPaging && ( + + )} + + )} + ); } diff --git a/components/input/WebsiteSelect.js b/components/input/WebsiteSelect.js index b77ae57c..ae3ceb46 100644 --- a/components/input/WebsiteSelect.js +++ b/components/input/WebsiteSelect.js @@ -8,12 +8,12 @@ export function WebsiteSelect({ websiteId, onSelect }) { const { data } = useQuery(['websites:me'], () => get('/me/websites')); const renderValue = value => { - return data?.find(({ id }) => id === value)?.name; + return data?.data?.find(({ id }) => id === value)?.name; }; return ( - get('/websites', { userId, includeTeams: 1 }), + get('/websites', { includeTeams: 1 }), ); - const hasData = data && data.length !== 0; + const hasData = data && data?.data.length !== 0; + const { dir } = useLocale(); function handleMore() { @@ -47,8 +48,10 @@ export function Dashboard({ userId }) { )} {hasData && ( <> - {editing && } - {!editing && } + {editing && } + {!editing && ( + + )} {max < data.length && ( - - + {hasData && ( +
+ + + {({ id, name }) => {name}} + + + + + + {formatMessage(labels.addWebsite)} + + + + + )} ); } diff --git a/components/pages/settings/teams/TeamMembers.js b/components/pages/settings/teams/TeamMembers.js index 3ea8232c..9762ef29 100644 --- a/components/pages/settings/teams/TeamMembers.js +++ b/components/pages/settings/teams/TeamMembers.js @@ -2,13 +2,22 @@ import { Loading, useToasts } from 'react-basics'; import TeamMembersTable from 'components/pages/settings/teams/TeamMembersTable'; import useApi from 'hooks/useApi'; import useMessages from 'hooks/useMessages'; +import useApiFilter from 'hooks/useApiFilter'; export function TeamMembers({ teamId, readOnly }) { const { showToast } = useToasts(); - const { get, useQuery } = useApi(); const { formatMessage, messages } = useMessages(); - const { data, isLoading, refetch } = useQuery(['teams:users', teamId], () => - get(`/teams/${teamId}/users`), + const { filter, page, pageSize, handleFilterChange, handlePageChange, handlePageSizeChange } = + useApiFilter(); + const { get, useQuery } = useApi(); + const { data, isLoading, refetch } = useQuery( + ['teams:users', teamId, filter, page, pageSize], + () => + get(`/teams/${teamId}/users`, { + filter, + page, + pageSize, + }), ); if (isLoading) { @@ -22,7 +31,15 @@ export function TeamMembers({ teamId, readOnly }) { return ( <> - + ); } diff --git a/components/pages/settings/teams/TeamMembersTable.js b/components/pages/settings/teams/TeamMembersTable.js index 8e6fad82..daa4acc6 100644 --- a/components/pages/settings/teams/TeamMembersTable.js +++ b/components/pages/settings/teams/TeamMembersTable.js @@ -4,7 +4,15 @@ import { ROLES } from 'lib/constants'; import TeamMemberRemoveButton from './TeamMemberRemoveButton'; import SettingsTable from 'components/common/SettingsTable'; -export function TeamMembersTable({ data = [], onSave, readOnly }) { +export function TeamMembersTable({ + data = [], + onSave, + readOnly, + filterValue, + onFilterChange, + onPageChange, + onPageSizeChange, +}) { const { formatMessage, labels } = useMessages(); const { user } = useUser(); @@ -16,7 +24,7 @@ export function TeamMembersTable({ data = [], onSave, readOnly }) { const cellRender = (row, data, key) => { if (key === 'username') { - return row?.user?.username; + return row?.username; } if (key === 'role') { return formatMessage( @@ -27,13 +35,23 @@ export function TeamMembersTable({ data = [], onSave, readOnly }) { }; return ( - + {row => { return ( !readOnly && ( diff --git a/components/pages/settings/teams/TeamWebsites.js b/components/pages/settings/teams/TeamWebsites.js index 9a5761e5..2ae344f5 100644 --- a/components/pages/settings/teams/TeamWebsites.js +++ b/components/pages/settings/teams/TeamWebsites.js @@ -13,13 +13,22 @@ import TeamWebsitesTable from 'components/pages/settings/teams/TeamWebsitesTable import TeamAddWebsiteForm from 'components/pages/settings/teams/TeamAddWebsiteForm'; import useApi from 'hooks/useApi'; import useMessages from 'hooks/useMessages'; +import useApiFilter from 'hooks/useApiFilter'; export function TeamWebsites({ teamId }) { const { showToast } = useToasts(); const { formatMessage, labels, messages } = useMessages(); + const { filter, page, pageSize, handleFilterChange, handlePageChange, handlePageSizeChange } = + useApiFilter(); const { get, useQuery } = useApi(); - const { data, isLoading, refetch } = useQuery(['teams:websites', teamId], () => - get(`/teams/${teamId}/websites`), + const { data, isLoading, refetch } = useQuery( + ['teams:websites', teamId, filter, page, pageSize], + () => + get(`/teams/${teamId}/websites`, { + filter, + page, + pageSize, + }), ); const hasData = data && data.length !== 0; @@ -49,7 +58,17 @@ export function TeamWebsites({ teamId }) { return (
{addButton} - {hasData && } + {hasData && ( + + )}
); } diff --git a/components/pages/settings/teams/TeamWebsitesTable.js b/components/pages/settings/teams/TeamWebsitesTable.js index 4873c6c7..564c8a78 100644 --- a/components/pages/settings/teams/TeamWebsitesTable.js +++ b/components/pages/settings/teams/TeamWebsitesTable.js @@ -6,9 +6,17 @@ import TeamWebsiteRemoveButton from './TeamWebsiteRemoveButton'; import SettingsTable from 'components/common/SettingsTable'; import useConfig from 'hooks/useConfig'; -export function TeamWebsitesTable({ data = [], onSave }) { +export function TeamWebsitesTable({ + data = [], + onSave, + filterValue, + onFilterChange, + onPageChange, + onPageSizeChange, +}) { const { formatMessage, labels } = useMessages(); const { openExternal } = useConfig(); + const { user } = useUser(); const columns = [ { name: 'name', label: formatMessage(labels.name) }, @@ -17,11 +25,19 @@ export function TeamWebsitesTable({ data = [], onSave }) { ]; return ( - + {row => { - const { teamId } = row; - const { id: websiteId, name, domain, userId } = row.website; - const { teamUser } = row.team; + const { id: teamId, teamUser } = row.teamWebsite[0].team; + const { id: websiteId, name, domain, userId } = row; const owner = teamUser[0]; const canRemove = user.id === userId || user.id === owner.userId; diff --git a/components/pages/settings/teams/TeamsList.js b/components/pages/settings/teams/TeamsList.js index 0c82639b..061100f6 100644 --- a/components/pages/settings/teams/TeamsList.js +++ b/components/pages/settings/teams/TeamsList.js @@ -1,24 +1,37 @@ -import { useState } from 'react'; -import { Button, Icon, Modal, ModalTrigger, useToasts, Text, Flexbox } from 'react-basics'; import EmptyPlaceholder from 'components/common/EmptyPlaceholder'; -import TeamAddForm from 'components/pages/settings/teams/TeamAddForm'; -import PageHeader from 'components/layout/PageHeader'; -import TeamsTable from 'components/pages/settings/teams/TeamsTable'; -import Page from 'components/layout/Page'; import Icons from 'components/icons'; -import TeamJoinForm from './TeamJoinForm'; +import Page from 'components/layout/Page'; +import PageHeader from 'components/layout/PageHeader'; +import TeamAddForm from 'components/pages/settings/teams/TeamAddForm'; +import TeamsTable from 'components/pages/settings/teams/TeamsTable'; import useApi from 'hooks/useApi'; import useMessages from 'hooks/useMessages'; -import { ROLES } from 'lib/constants'; import useUser from 'hooks/useUser'; +import { ROLES } from 'lib/constants'; +import { useState } from 'react'; +import { Button, Flexbox, Icon, Modal, ModalTrigger, Text, useToasts } from 'react-basics'; +import TeamJoinForm from './TeamJoinForm'; +import useApiFilter from 'hooks/useApiFilter'; export default function TeamsList() { const { user } = useUser(); const { formatMessage, labels, messages } = useMessages(); + const { filter, page, pageSize, handleFilterChange, handlePageChange, handlePageSizeChange } = + useApiFilter(); const [update, setUpdate] = useState(0); + const { get, useQuery } = useApi(); - const { data, isLoading, error } = useQuery(['teams', update], () => get(`/teams`)); - const hasData = data && data.length !== 0; + const { data, isLoading, error } = useQuery(['teams', update, filter, page, pageSize], () => { + return get(`/teams`, { + filter, + page, + pageSize, + }); + }); + + const hasData = data && data?.data.length !== 0; + const isFiltered = filter; + const { showToast } = useToasts(); const handleSave = () => { @@ -71,15 +84,26 @@ export default function TeamsList() { return ( - {hasData && ( + {(hasData || isFiltered) && ( {joinButton} {createButton} )} - {hasData && } - {!hasData && ( + + {(hasData || isFiltered) && ( + + )} + + {!hasData && !isFiltered && ( {joinButton} diff --git a/components/pages/settings/teams/TeamsTable.js b/components/pages/settings/teams/TeamsTable.js index a344fefc..e35fb839 100644 --- a/components/pages/settings/teams/TeamsTable.js +++ b/components/pages/settings/teams/TeamsTable.js @@ -1,14 +1,21 @@ +import SettingsTable from 'components/common/SettingsTable'; +import useLocale from 'hooks/useLocale'; +import useMessages from 'hooks/useMessages'; +import useUser from 'hooks/useUser'; +import { ROLES } from 'lib/constants'; import Link from 'next/link'; import { Button, Icon, Icons, Modal, ModalTrigger, Text } from 'react-basics'; import TeamDeleteForm from './TeamDeleteForm'; import TeamLeaveForm from './TeamLeaveForm'; -import useMessages from 'hooks/useMessages'; -import useUser from 'hooks/useUser'; -import { ROLES } from 'lib/constants'; -import SettingsTable from 'components/common/SettingsTable'; -import useLocale from 'hooks/useLocale'; -export function TeamsTable({ data = [], onDelete }) { +export function TeamsTable({ + data = { data: [] }, + onDelete, + filterValue, + onFilterChange, + onPageChange, + onPageSizeChange, +}) { const { formatMessage, labels } = useMessages(); const { user } = useUser(); const { dir } = useLocale(); @@ -27,7 +34,17 @@ export function TeamsTable({ data = [], onDelete }) { }; return ( - + {row => { const { id, teamUser } = row; const owner = teamUser.find(({ role }) => role === ROLES.teamOwner); diff --git a/components/pages/settings/users/UsersList.js b/components/pages/settings/users/UsersList.js index 8886203b..614aabef 100644 --- a/components/pages/settings/users/UsersList.js +++ b/components/pages/settings/users/UsersList.js @@ -7,14 +7,27 @@ import UserAddButton from './UserAddButton'; import useApi from 'hooks/useApi'; import useUser from 'hooks/useUser'; import useMessages from 'hooks/useMessages'; +import useApiFilter from 'hooks/useApiFilter'; export function UsersList() { const { formatMessage, labels, messages } = useMessages(); const { user } = useUser(); + const { filter, page, pageSize, handleFilterChange, handlePageChange, handlePageSizeChange } = + useApiFilter(); + const { get, useQuery } = useApi(); - const { data, isLoading, error, refetch } = useQuery(['user'], () => get(`/users`), { - enabled: !!user, - }); + const { data, isLoading, error, refetch } = useQuery( + ['user', filter, page, pageSize], + () => + get(`/users`, { + filter, + page, + pageSize, + }), + { + enabled: !!user, + }, + ); const { showToast } = useToasts(); const hasData = data && data.length !== 0; @@ -33,8 +46,17 @@ export function UsersList() { - {hasData && } - {!hasData && ( + {(hasData || filter) && ( + + )} + {!hasData && !filter && ( diff --git a/components/pages/settings/users/UsersTable.js b/components/pages/settings/users/UsersTable.js index 2023efc5..f4c9dd77 100644 --- a/components/pages/settings/users/UsersTable.js +++ b/components/pages/settings/users/UsersTable.js @@ -8,7 +8,14 @@ import useMessages from 'hooks/useMessages'; import SettingsTable from 'components/common/SettingsTable'; import useLocale from 'hooks/useLocale'; -export function UsersTable({ data = [], onDelete }) { +export function UsersTable({ + data = { data: [] }, + onDelete, + filterValue, + onFilterChange, + onPageChange, + onPageSizeChange, +}) { const { formatMessage, labels } = useMessages(); const { user } = useUser(); const { dateLocale } = useLocale(); @@ -36,7 +43,17 @@ export function UsersTable({ data = [], onDelete }) { }; return ( - + {(row, keys, rowIndex) => { return ( <> diff --git a/components/pages/settings/websites/WebsitesList.js b/components/pages/settings/websites/WebsitesList.js index de423d0b..310b481f 100644 --- a/components/pages/settings/websites/WebsitesList.js +++ b/components/pages/settings/websites/WebsitesList.js @@ -8,14 +8,22 @@ import useApi from 'hooks/useApi'; import useUser from 'hooks/useUser'; import useMessages from 'hooks/useMessages'; import { ROLES } from 'lib/constants'; +import useApiFilter from 'hooks/useApiFilter'; export function WebsitesList() { const { formatMessage, labels, messages } = useMessages(); const { user } = useUser(); + const { filter, page, pageSize, handleFilterChange, handlePageChange, handlePageSizeChange } = + useApiFilter(); const { get, useQuery } = useApi(); const { data, isLoading, error, refetch } = useQuery( - ['websites', user?.id], - () => get(`/users/${user?.id}/websites`), + ['websites', user?.id, filter, page, pageSize], + () => + get(`/users/${user?.id}/websites`, { + filter, + page, + pageSize, + }), { enabled: !!user }, ); const { showToast } = useToasts(); @@ -47,7 +55,15 @@ export function WebsitesList() { return ( {addButton} - {hasData && } + {hasData && ( + + )} {!hasData && ( {addButton} diff --git a/components/pages/settings/websites/WebsitesTable.js b/components/pages/settings/websites/WebsitesTable.js index 902393e6..aa8cbe8a 100644 --- a/components/pages/settings/websites/WebsitesTable.js +++ b/components/pages/settings/websites/WebsitesTable.js @@ -4,7 +4,13 @@ import SettingsTable from 'components/common/SettingsTable'; import useMessages from 'hooks/useMessages'; import useConfig from 'hooks/useConfig'; -export function WebsitesTable({ data = [] }) { +export function WebsitesTable({ + data = [], + filterValue, + onFilterChange, + onPageChange, + onPageSizeChange, +}) { const { formatMessage, labels } = useMessages(); const { openExternal } = useConfig(); @@ -15,7 +21,16 @@ export function WebsitesTable({ data = [] }) { ]; return ( - + {row => { const { id } = row; diff --git a/components/pages/websites/WebsiteReportsPage.js b/components/pages/websites/WebsiteReportsPage.js index 56927028..a1d49d10 100644 --- a/components/pages/websites/WebsiteReportsPage.js +++ b/components/pages/websites/WebsiteReportsPage.js @@ -7,7 +7,16 @@ import WebsiteHeader from './WebsiteHeader'; export function WebsiteReportsPage({ websiteId }) { const { formatMessage, labels } = useMessages(); - const { reports, error, isLoading, deleteReport } = useReports(websiteId); + const { + reports, + error, + isLoading, + deleteReport, + filter, + handleFilterChange, + handlePageChange, + handlePageSizeChange, + } = useReports(websiteId); const handleDelete = async id => { await deleteReport(id); @@ -26,7 +35,14 @@ export function WebsiteReportsPage({ websiteId }) { - + ); } diff --git a/hooks/useApiFilter.ts b/hooks/useApiFilter.ts new file mode 100644 index 00000000..d411fd43 --- /dev/null +++ b/hooks/useApiFilter.ts @@ -0,0 +1,28 @@ +import { useState } from 'react'; + +export function useApiFilter() { + const [filter, setFilter] = useState(); + const [filterType, setFilterType] = useState('All'); + const [page, setPage] = useState(1); + const [pageSize, setPageSize] = useState(10); + + const handleFilterChange = value => setFilter(value); + const handlePageChange = value => setPage(value); + const handlePageSizeChange = value => setPageSize(value); + + return { + filter, + setFilter, + filterType, + setFilterType, + page, + setPage, + pageSize, + setPageSize, + handleFilterChange, + handlePageChange, + handlePageSizeChange, + }; +} + +export default useApiFilter; diff --git a/hooks/useReports.js b/hooks/useReports.js index f4369eec..57d76492 100644 --- a/hooks/useReports.js +++ b/hooks/useReports.js @@ -1,12 +1,16 @@ import { useState } from 'react'; import useApi from './useApi'; +import useApiFilter from 'hooks/useApiFilter'; export function useReports(websiteId) { const [modified, setModified] = useState(Date.now()); const { get, useQuery, del, useMutation } = useApi(); const { mutate } = useMutation(reportId => del(`/reports/${reportId}`)); - const { data, error, isLoading } = useQuery(['reports:website', { websiteId, modified }], () => - get(`/reports`, { websiteId }), + const { filter, page, pageSize, handleFilterChange, handlePageChange, handlePageSizeChange } = + useApiFilter(); + const { data, error, isLoading } = useQuery( + ['reports:website', { websiteId, modified, filter, page, pageSize }], + () => get(`/reports`, { websiteId, filter, page, pageSize }), ); const deleteReport = id => { @@ -17,7 +21,18 @@ export function useReports(websiteId) { }); }; - return { reports: data, error, isLoading, deleteReport }; + return { + reports: data, + error, + isLoading, + deleteReport, + filter, + page, + pageSize, + handleFilterChange, + handlePageChange, + handlePageSizeChange, + }; } export default useReports; diff --git a/lib/constants.ts b/lib/constants.ts index 887f90a9..9257298c 100644 --- a/lib/constants.ts +++ b/lib/constants.ts @@ -30,6 +30,22 @@ export const FILTER_RANGE = 'filter-range'; export const FILTER_REFERRERS = 'filter-referrers'; export const FILTER_PAGES = 'filter-pages'; +export const USER_FILTER_TYPES = { + all: 'All', + username: 'Username', +} as const; +export const WEBSITE_FILTER_TYPES = { all: 'All', name: 'Name', domain: 'Domain' } as const; +export const TEAM_FILTER_TYPES = { all: 'All', name: 'Name', 'user:username': 'Owner' } as const; +export const REPORT_FILTER_TYPES = { + all: 'All', + name: 'Name', + description: 'Description', + type: 'Type', + 'user:username': 'Username', + 'website:name': 'Website Name', + 'website:domain': 'Website Domain', +} as const; + export const EVENT_COLUMNS = ['url', 'referrer', 'title', 'query', 'event']; export const SESSION_COLUMNS = [ diff --git a/lib/prisma.ts b/lib/prisma.ts index 753f1ae4..c67ce4bc 100644 --- a/lib/prisma.ts +++ b/lib/prisma.ts @@ -4,7 +4,7 @@ import { MYSQL, POSTGRESQL, getDatabaseType } from 'lib/db'; import { FILTER_COLUMNS, SESSION_COLUMNS } from './constants'; import { loadWebsite } from './load'; import { maxDate } from './date'; -import { QueryFilters, QueryOptions } from './types'; +import { QueryFilters, QueryOptions, SearchFilter } from './types'; const MYSQL_DATE_FORMATS = { minute: '%Y-%m-%d %H:%i:00', @@ -128,6 +128,37 @@ async function rawQuery(sql: string, data: object): Promise { return prisma.rawQuery(query, params); } +function getPageFilters(filters: SearchFilter): [ + { + orderBy: { + [x: string]: string; + }[]; + take: number; + skip: number; + }, + { + pageSize: number; + page: number; + orderBy: string; + }, +] { + const { pageSize = 10, page = 1, orderBy } = filters; + + return [ + { + ...(pageSize > 0 && { take: pageSize, skip: pageSize * (page - 1) }), + ...(orderBy && { + orderBy: [ + { + [orderBy]: 'asc', + }, + ], + }), + }, + { pageSize, page: +page, orderBy }, + ]; +} + export default { ...prisma, getAddMinutesQuery, @@ -135,5 +166,6 @@ export default { getTimestampIntervalQuery, getFilterQuery, parseFilters, + getPageFilters, rawQuery, }; diff --git a/lib/types.ts b/lib/types.ts index dc54fd47..5a25169a 100644 --- a/lib/types.ts +++ b/lib/types.ts @@ -1,17 +1,62 @@ import { NextApiRequest } from 'next'; -import { COLLECTION_TYPE, DATA_TYPE, EVENT_TYPE, KAFKA_TOPIC, ROLES } from './constants'; +import { + COLLECTION_TYPE, + DATA_TYPE, + EVENT_TYPE, + KAFKA_TOPIC, + REPORT_FILTER_TYPES, + ROLES, + TEAM_FILTER_TYPES, + USER_FILTER_TYPES, + WEBSITE_FILTER_TYPES, +} from './constants'; type ObjectValues = T[keyof T]; export type CollectionType = ObjectValues; - export type Role = ObjectValues; - export type EventType = ObjectValues; - export type DynamicDataType = ObjectValues; - export type KafkaTopic = ObjectValues; +export type ReportSearchFilterType = ObjectValues; +export type UserSearchFilterType = ObjectValues; +export type WebsiteSearchFilterType = ObjectValues; +export type TeamSearchFilterType = ObjectValues; + +export interface WebsiteSearchFilter extends SearchFilter { + userId?: string; + teamId?: string; + includeTeams?: boolean; +} + +export interface UserSearchFilter extends SearchFilter { + teamId?: string; +} + +export interface TeamSearchFilter extends SearchFilter { + userId?: string; +} + +export interface ReportSearchFilter extends SearchFilter { + userId?: string; + websiteId?: string; +} + +export interface SearchFilter { + filter?: string; + filterType?: T; + pageSize?: number; + page?: number; + orderBy?: string; +} + +export interface FilterResult { + data: T; + count: number; + pageSize: number; + page: number; + orderBy?: string; +} export interface DynamicData { [key: string]: number | string | DynamicData | number[] | string[] | DynamicData[]; diff --git a/package.json b/package.json index 647cdf41..89dc5e97 100644 --- a/package.json +++ b/package.json @@ -94,7 +94,7 @@ "node-fetch": "^3.2.8", "npm-run-all": "^4.1.5", "react": "^18.2.0", - "react-basics": "^0.91.0", + "react-basics": "^0.92.0", "react-beautiful-dnd": "^13.1.0", "react-dom": "^18.2.0", "react-error-boundary": "^4.0.4", diff --git a/pages/api/reports/index.ts b/pages/api/reports/index.ts index c856b565..8c6825f1 100644 --- a/pages/api/reports/index.ts +++ b/pages/api/reports/index.ts @@ -1,10 +1,12 @@ -import { useAuth, useCors } from 'lib/middleware'; -import { NextApiRequestQueryBody } from 'lib/types'; -import { NextApiResponse } from 'next'; -import { methodNotAllowed, ok, unauthorized } from 'next-basics'; -import { createReport, getWebsiteReports } from 'queries'; import { canViewWebsite } from 'lib/auth'; import { uuid } from 'lib/crypto'; +import { useAuth, useCors } from 'lib/middleware'; +import { NextApiRequestQueryBody, ReportSearchFilterType, SearchFilter } from 'lib/types'; +import { NextApiResponse } from 'next'; +import { methodNotAllowed, ok, unauthorized } from 'next-basics'; +import { createReport, getReportsByWebsiteId } from 'queries'; + +export interface ReportsRequestQuery extends SearchFilter {} export interface ReportRequestBody { websiteId: string; @@ -35,7 +37,13 @@ export default async ( return unauthorized(res); } - const data = await getWebsiteReports(websiteId); + const { page, filter, pageSize } = req.query; + + const data = await getReportsByWebsiteId(websiteId, { + page, + filter, + pageSize: +pageSize || null, + }); return ok(res, data); } diff --git a/pages/api/teams/[id]/users/index.ts b/pages/api/teams/[id]/users/index.ts index c73da683..6f8b077e 100644 --- a/pages/api/teams/[id]/users/index.ts +++ b/pages/api/teams/[id]/users/index.ts @@ -1,11 +1,11 @@ import { canUpdateTeam, canViewTeam } from 'lib/auth'; import { useAuth } from 'lib/middleware'; -import { NextApiRequestQueryBody } from 'lib/types'; +import { NextApiRequestQueryBody, SearchFilter, TeamSearchFilterType } from 'lib/types'; import { NextApiResponse } from 'next'; import { badRequest, methodNotAllowed, ok, unauthorized } from 'next-basics'; -import { createTeamUser, getTeamUsers, getUserByUsername } from 'queries'; +import { createTeamUser, getUserByUsername, getUsersByTeamId } from 'queries'; -export interface TeamUserRequestQuery { +export interface TeamUserRequestQuery extends SearchFilter { id: string; } @@ -27,7 +27,13 @@ export default async ( return unauthorized(res); } - const users = await getTeamUsers(teamId); + const { page, filter, pageSize } = req.query; + + const users = await getUsersByTeamId(teamId, { + page, + filter, + pageSize: +pageSize || null, + }); return ok(res, users); } diff --git a/pages/api/teams/[id]/websites/index.ts b/pages/api/teams/[id]/websites/index.ts index 63be478b..dcd08939 100644 --- a/pages/api/teams/[id]/websites/index.ts +++ b/pages/api/teams/[id]/websites/index.ts @@ -1,11 +1,12 @@ import { canViewTeam } from 'lib/auth'; import { useAuth } from 'lib/middleware'; -import { NextApiRequestQueryBody } from 'lib/types'; +import { NextApiRequestQueryBody, SearchFilter, WebsiteSearchFilterType } from 'lib/types'; import { NextApiResponse } from 'next'; import { methodNotAllowed, ok, unauthorized } from 'next-basics'; -import { createTeamWebsites, getTeamWebsites } from 'queries/admin/teamWebsite'; +import { getWebsites, getWebsitesByTeamId } from 'queries'; +import { createTeamWebsites } from 'queries/admin/teamWebsite'; -export interface TeamWebsiteRequestQuery { +export interface TeamWebsiteRequestQuery extends SearchFilter { id: string; } @@ -26,7 +27,13 @@ export default async ( return unauthorized(res); } - const websites = await getTeamWebsites(teamId); + const { page, filter, pageSize } = req.query; + + const websites = await getWebsitesByTeamId(teamId, { + page, + filter, + pageSize: +pageSize || null, + }); return ok(res, websites); } diff --git a/pages/api/teams/index.ts b/pages/api/teams/index.ts index 453f1ef3..997ed885 100644 --- a/pages/api/teams/index.ts +++ b/pages/api/teams/index.ts @@ -1,18 +1,19 @@ import { Team } from '@prisma/client'; -import { NextApiRequestQueryBody } from 'lib/types'; import { canCreateTeam } from 'lib/auth'; import { uuid } from 'lib/crypto'; import { useAuth } from 'lib/middleware'; +import { NextApiRequestQueryBody, SearchFilter, TeamSearchFilterType } from 'lib/types'; import { NextApiResponse } from 'next'; import { getRandomChars, methodNotAllowed, ok, unauthorized } from 'next-basics'; -import { createTeam, getUserTeams } from 'queries'; +import { createTeam, getTeamsByUserId } from 'queries'; -export interface TeamsRequestBody { +export interface TeamsRequestQuery extends SearchFilter {} +export interface TeamsRequestBody extends SearchFilter { name: string; } export default async ( - req: NextApiRequestQueryBody, + req: NextApiRequestQueryBody, res: NextApiResponse, ) => { await useAuth(req, res); @@ -22,9 +23,11 @@ export default async ( } = req.auth; if (req.method === 'GET') { - const teams = await getUserTeams(userId); + const { page, filter, pageSize } = req.query; - return ok(res, teams); + const results = await getTeamsByUserId(userId, { page, filter, pageSize: +pageSize || null }); + + return ok(res, results); } if (req.method === 'POST') { diff --git a/pages/api/users/[id]/websites.ts b/pages/api/users/[id]/websites.ts index e94094a4..e1761291 100644 --- a/pages/api/users/[id]/websites.ts +++ b/pages/api/users/[id]/websites.ts @@ -1,9 +1,12 @@ import { useAuth, useCors } from 'lib/middleware'; -import { NextApiRequestQueryBody } from 'lib/types'; +import { NextApiRequestQueryBody, SearchFilter, WebsiteSearchFilterType } from 'lib/types'; import { NextApiResponse } from 'next'; import { methodNotAllowed, ok, unauthorized } from 'next-basics'; -import { getUserWebsites } from 'queries'; +import { getWebsitesByUserId } from 'queries'; +export interface UserWebsitesRequestQuery extends SearchFilter { + id: string; +} export interface UserWebsitesRequestBody { name: string; domain: string; @@ -17,16 +20,19 @@ export default async ( await useCors(req, res); await useAuth(req, res); const { user } = req.auth; - const { id: userId } = req.query; + const { id: userId, page, filter, pageSize, includeTeams } = req.query; if (req.method === 'GET') { if (!user.isAdmin && user.id !== userId) { return unauthorized(res); } - const { includeTeams } = req.query; - - const websites = await getUserWebsites(userId, { includeTeams }); + const websites = await getWebsitesByUserId(userId, { + page, + filter, + pageSize: +pageSize || null, + includeTeams, + }); return ok(res, websites); } diff --git a/pages/api/users/index.ts b/pages/api/users/index.ts index 6f6c205f..5e913c02 100644 --- a/pages/api/users/index.ts +++ b/pages/api/users/index.ts @@ -2,11 +2,12 @@ import { canCreateUser, canViewUsers } from 'lib/auth'; import { ROLES } from 'lib/constants'; import { uuid } from 'lib/crypto'; import { useAuth } from 'lib/middleware'; -import { NextApiRequestQueryBody, Role, User } from 'lib/types'; +import { NextApiRequestQueryBody, Role, SearchFilter, User, UserSearchFilterType } from 'lib/types'; import { NextApiResponse } from 'next'; import { badRequest, hashPassword, methodNotAllowed, ok, unauthorized } from 'next-basics'; import { createUser, getUserByUsername, getUsers } from 'queries'; +export interface UsersRequestQuery extends SearchFilter {} export interface UsersRequestBody { username: string; password: string; @@ -15,7 +16,7 @@ export interface UsersRequestBody { } export default async ( - req: NextApiRequestQueryBody, + req: NextApiRequestQueryBody, res: NextApiResponse, ) => { await useAuth(req, res); @@ -25,7 +26,9 @@ export default async ( return unauthorized(res); } - const users = await getUsers(); + const { page, filter, pageSize } = req.query; + + const users = await getUsers({ page, filter, pageSize: +pageSize || null }); return ok(res, users); } diff --git a/pages/api/websites/index.ts b/pages/api/websites/index.ts index c8b5aba2..f94fa037 100644 --- a/pages/api/websites/index.ts +++ b/pages/api/websites/index.ts @@ -1,12 +1,14 @@ import { canCreateWebsite } from 'lib/auth'; import { uuid } from 'lib/crypto'; import { useAuth, useCors } from 'lib/middleware'; -import { NextApiRequestQueryBody } from 'lib/types'; +import { NextApiRequestQueryBody, SearchFilter, WebsiteSearchFilterType } from 'lib/types'; import { NextApiResponse } from 'next'; import { methodNotAllowed, ok, unauthorized } from 'next-basics'; import { createWebsite } from 'queries'; import userWebsites from 'pages/api/users/[id]/websites'; +export interface WebsitesRequestQuery extends SearchFilter {} + export interface WebsitesRequestBody { name: string; domain: string; @@ -14,7 +16,7 @@ export interface WebsitesRequestBody { } export default async ( - req: NextApiRequestQueryBody, + req: NextApiRequestQueryBody, res: NextApiResponse, ) => { await useCors(req, res); @@ -26,6 +28,7 @@ export default async ( if (req.method === 'GET') { req.query.id = userId; + req.query.pageSize = 100; return userWebsites(req, res); } diff --git a/queries/admin/report.ts b/queries/admin/report.ts index ee7a0592..d2523f82 100644 --- a/queries/admin/report.ts +++ b/queries/admin/report.ts @@ -1,5 +1,7 @@ import { Prisma, Report } from '@prisma/client'; +import { REPORT_FILTER_TYPES } from 'lib/constants'; import prisma from 'lib/prisma'; +import { FilterResult, ReportSearchFilter, ReportSearchFilterType, SearchFilter } from 'lib/types'; export async function createReport(data: Prisma.ReportUncheckedCreateInput): Promise { return prisma.client.report.create({ data }); @@ -13,22 +15,6 @@ export async function getReportById(reportId: string): Promise { }); } -export async function getUserReports(userId: string): Promise { - return prisma.client.report.findMany({ - where: { - userId, - }, - }); -} - -export async function getWebsiteReports(websiteId: string): Promise { - return prisma.client.report.findMany({ - where: { - websiteId, - }, - }); -} - export async function updateReport( reportId: string, data: Prisma.ReportUpdateInput, @@ -39,3 +25,103 @@ export async function updateReport( export async function deleteReport(reportId: string): Promise { return prisma.client.report.delete({ where: { id: reportId } }); } + +export async function getReports( + ReportSearchFilter: ReportSearchFilter, +): Promise> { + const { userId, websiteId, filter, filterType = REPORT_FILTER_TYPES.all } = ReportSearchFilter; + const where: Prisma.ReportWhereInput = { + ...(userId && { userId: userId }), + ...(websiteId && { websiteId: websiteId }), + ...(filter && { + AND: { + OR: [ + { + ...((filterType === REPORT_FILTER_TYPES.all || + filterType === REPORT_FILTER_TYPES.name) && { + name: { + startsWith: filter, + }, + }), + }, + { + ...((filterType === REPORT_FILTER_TYPES.all || + filterType === REPORT_FILTER_TYPES.description) && { + description: { + startsWith: filter, + }, + }), + }, + { + ...((filterType === REPORT_FILTER_TYPES.all || + filterType === REPORT_FILTER_TYPES.type) && { + type: { + startsWith: filter, + }, + }), + }, + { + ...((filterType === REPORT_FILTER_TYPES.all || + filterType === REPORT_FILTER_TYPES['user:username']) && { + user: { + username: { + startsWith: filter, + }, + }, + }), + }, + { + ...((filterType === REPORT_FILTER_TYPES.all || + filterType === REPORT_FILTER_TYPES['website:name']) && { + website: { + name: { + startsWith: filter, + }, + }, + }), + }, + { + ...((filterType === REPORT_FILTER_TYPES.all || + filterType === REPORT_FILTER_TYPES['website:domain']) && { + website: { + domain: { + startsWith: filter, + }, + }, + }), + }, + ], + }, + }), + }; + + const [pageFilters, getParameters] = prisma.getPageFilters(ReportSearchFilter); + + const reports = await prisma.client.report.findMany({ + where, + ...pageFilters, + }); + const count = await prisma.client.report.count({ + where, + }); + + return { + data: reports, + count, + ...getParameters, + }; +} + +export async function getReportsByUserId( + userId: string, + filter: SearchFilter, +): Promise> { + return getReports({ userId, ...filter }); +} + +export async function getReportsByWebsiteId( + websiteId: string, + filter: SearchFilter, +): Promise> { + return getReports({ websiteId, ...filter }); +} diff --git a/queries/admin/team.ts b/queries/admin/team.ts index a8b3385c..97838227 100644 --- a/queries/admin/team.ts +++ b/queries/admin/team.ts @@ -1,7 +1,8 @@ import { Prisma, Team } from '@prisma/client'; import prisma from 'lib/prisma'; -import { ROLES } from 'lib/constants'; +import { ROLES, TEAM_FILTER_TYPES } from 'lib/constants'; import { uuid } from 'lib/crypto'; +import { FilterResult, TeamSearchFilter, TeamSearchFilterType, SearchFilter } from 'lib/types'; export interface GetTeamOptions { includeTeamUser?: boolean; @@ -26,12 +27,6 @@ export function getTeamByAccessCode(accessCode: string, options: GetTeamOptions return getTeam({ accessCode }, options); } -export async function getTeams(where: Prisma.TeamWhereInput): Promise { - return prisma.client.team.findMany({ - where, - }); -} - export async function createTeam(data: Prisma.TeamCreateInput, userId: string): Promise { const { id } = data; @@ -85,3 +80,82 @@ export async function deleteTeam( }), ]); } + +export async function getTeams( + TeamSearchFilter: TeamSearchFilter, + options?: { include?: Prisma.TeamInclude }, +): Promise> { + const { userId, filter, filterType = TEAM_FILTER_TYPES.all } = TeamSearchFilter; + const where: Prisma.TeamWhereInput = { + ...(userId && { + teamUser: { + some: { userId }, + }, + }), + ...(filter && { + AND: { + OR: [ + { + ...((filterType === TEAM_FILTER_TYPES.all || filterType === TEAM_FILTER_TYPES.name) && { + name: { startsWith: filter }, + }), + }, + { + ...((filterType === TEAM_FILTER_TYPES.all || + filterType === TEAM_FILTER_TYPES['user:username']) && { + teamUser: { + every: { + role: ROLES.teamOwner, + user: { + username: { + startsWith: filter, + }, + }, + }, + }, + }), + }, + ], + }, + }), + }; + + const [pageFilters, getParameters] = prisma.getPageFilters({ + orderBy: 'name', + ...TeamSearchFilter, + }); + + const teams = await prisma.client.team.findMany({ + where: { + ...where, + }, + ...pageFilters, + ...(options?.include && { include: options?.include }), + }); + const count = await prisma.client.team.count({ where }); + + return { data: teams, count, ...getParameters }; +} + +export async function getTeamsByUserId( + userId: string, + filter?: SearchFilter, +): Promise> { + return getTeams( + { userId, ...filter }, + { + include: { + teamUser: { + include: { + user: { + select: { + id: true, + username: true, + }, + }, + }, + }, + }, + }, + ); +} diff --git a/queries/admin/user.ts b/queries/admin/user.ts index f60c4801..f4be4751 100644 --- a/queries/admin/user.ts +++ b/queries/admin/user.ts @@ -1,9 +1,9 @@ -import { Prisma, Team, TeamUser } from '@prisma/client'; -import { getRandomChars } from 'next-basics'; +import { Prisma } from '@prisma/client'; import cache from 'lib/cache'; -import { ROLES } from 'lib/constants'; +import { ROLES, USER_FILTER_TYPES } from 'lib/constants'; import prisma from 'lib/prisma'; -import { Website, User, Role } from 'lib/types'; +import { FilterResult, Role, User, UserSearchFilter } from 'lib/types'; +import { getRandomChars } from 'next-basics'; export interface GetUserOptions { includePassword?: boolean; @@ -36,125 +36,59 @@ export async function getUserByUsername(username: string, options: GetUserOption return getUser({ username }, options); } -export async function getUsers(): Promise { - return prisma.client.user.findMany({ - take: 100, - where: { - deletedAt: null, - }, - orderBy: [ - { - username: 'asc', - }, - ], - select: { - id: true, - username: true, - role: true, - createdAt: true, - }, - }); -} - -export async function getUserTeams(userId: string): Promise< - (Team & { - teamUser: (TeamUser & { - user: { id: string; username: string }; - })[]; - })[] -> { - return prisma.client.team.findMany({ - where: { +export async function getUsers( + UserSearchFilter: UserSearchFilter = {}, + options?: { include?: Prisma.UserInclude }, +): Promise> { + const { teamId, filter, filterType = USER_FILTER_TYPES.all } = UserSearchFilter; + const where: Prisma.UserWhereInput = { + ...(teamId && { teamUser: { some: { - userId, + teamId, }, }, - }, - include: { - teamUser: { - include: { - user: { - select: { - id: true, - username: true, - }, + }), + ...(filter && { + AND: { + OR: [ + { + ...((filterType === USER_FILTER_TYPES.all || + filterType === USER_FILTER_TYPES.username) && { + username: { + startsWith: filter, + }, + }), }, - }, + ], }, - }, + }), + }; + const [pageFilters, getParameters] = prisma.getPageFilters({ + orderBy: 'username', + ...UserSearchFilter, }); -} -export async function getUserWebsites( - userId: string, - options?: { includeTeams: boolean }, -): Promise { - 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({ + const users = await prisma.client.user.findMany({ where: { - userId, + ...where, deletedAt: null, }, - orderBy: [ - { - name: 'asc', - }, - ], + ...pageFilters, + ...(options?.include && { include: options.include }), }); + 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 }); } export async function createUser(data: { diff --git a/queries/admin/website.ts b/queries/admin/website.ts index 35f32bac..68f634a6 100644 --- a/queries/admin/website.ts +++ b/queries/admin/website.ts @@ -1,6 +1,8 @@ import { Prisma, Website } from '@prisma/client'; import cache from 'lib/cache'; +import { ROLES, WEBSITE_FILTER_TYPES } from 'lib/constants'; import prisma from 'lib/prisma'; +import { FilterResult, WebsiteSearchFilter } from 'lib/types'; async function getWebsite(where: Prisma.WebsiteWhereUniqueInput): Promise { return prisma.client.website.findUnique({ @@ -16,11 +18,199 @@ export async function getWebsiteByShareId(shareId: string) { return getWebsite({ shareId }); } -export async function getWebsites(): Promise { - return prisma.client.website.findMany({ - orderBy: { - name: 'asc', +export async function getWebsites( + WebsiteSearchFilter: WebsiteSearchFilter, + options?: { include?: Prisma.WebsiteInclude }, +): Promise> { + const { + userId, + teamId, + includeTeams, + filter, + filterType = WEBSITE_FILTER_TYPES.all, + } = WebsiteSearchFilter; + + const filterQuery = { + AND: { + OR: [ + { + ...((filterType === WEBSITE_FILTER_TYPES.all || + filterType === WEBSITE_FILTER_TYPES.name) && { + name: { startsWith: filter }, + }), + }, + { + ...((filterType === WEBSITE_FILTER_TYPES.all || + filterType === WEBSITE_FILTER_TYPES.domain) && { + domain: { startsWith: filter }, + }), + }, + ], }, + }; + + const where: Prisma.WebsiteWhereInput = { + ...(teamId && { + teamWebsite: { + some: { + teamId, + }, + }, + }), + AND: { + OR: [ + { + ...(userId && { + userId, + }), + }, + { + ...(includeTeams && { + teamWebsite: { + some: { + team: { + teamUser: { + some: { + userId, + }, + }, + }, + }, + }, + }), + }, + ], + }, + ...(filter && filterQuery), + }; + + const [pageFilters, getParameters] = prisma.getPageFilters({ + orderBy: 'name', + ...WebsiteSearchFilter, + }); + + 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 }); + + return { data: websites, count, ...getParameters }; +} + +export async function getWebsitesByUserId( + userId: string, + filter?: WebsiteSearchFilter, +): Promise> { + return getWebsites({ userId, ...filter }); +} + +export async function getWebsitesByTeamId( + teamId: string, + filter?: WebsiteSearchFilter, +): Promise> { + return getWebsites( + { + teamId, + ...filter, + includeTeams: true, + }, + { + include: { + teamWebsite: { + include: { + team: { + include: { + teamUser: { + where: { role: ROLES.teamOwner }, + }, + }, + }, + }, + }, + user: { + select: { + id: true, + username: true, + }, + }, + }, + }, + ); +} + +export async function getUserWebsites( + userId: string, + options?: { includeTeams: boolean }, +): Promise { + 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', + }, + ], }); } diff --git a/yarn.lock b/yarn.lock index d9224c2a..115e3cc9 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7557,10 +7557,10 @@ rc@^1.2.7: minimist "^1.2.0" strip-json-comments "~2.0.1" -react-basics@^0.91.0: - version "0.91.0" - resolved "https://registry.yarnpkg.com/react-basics/-/react-basics-0.91.0.tgz#2970529a22a455ec73a1be884eb93a109c9dafc0" - integrity sha512-vP8LYWiFwA+eguMEuHvHct4Jl5R/2GUjWc1tMujDG0CsAAUGhx68tAJr0K3gBrWjmpJrTPVfX8SdBNKSDAjQsw== +react-basics@^0.92.0: + version "0.92.0" + resolved "https://registry.yarnpkg.com/react-basics/-/react-basics-0.92.0.tgz#02bc6e88bdaf189c30cc6cbd8bbb1c9d12cd089b" + integrity sha512-BVUWg5a7R88konA9NedYMBx1hl50d6h/MD7qlKOEO/Cnm8cOC7AYTRKAKhO6kHMWjY4ZpUuvlg0UcF+SJP/uXA== dependencies: classnames "^2.3.1" date-fns "^2.29.3"