feat: refactoring casl permission checks for recursive nested operations (#778)

* feat: nested casl abilities

* fix: remove unused packages

* Fixes

* Fix createMany broken

* Fix lint

* Fix lint

* Fix lint

* Fix lint

* Fixes

* Fix CommentThread

* Fix bugs

* Fix lint

* Fix bugs

* Fixed auto routing

* Fixed app path

---------

Co-authored-by: Charles Bochet <charles@twenty.com>
Co-authored-by: Lucas Bordeau <bordeau.lucas@gmail.com>
This commit is contained in:
Jérémy M
2023-07-26 01:37:22 +02:00
committed by GitHub
parent 92b9e987a5
commit 51cfc0d82c
69 changed files with 1192 additions and 883 deletions

View File

@ -1,6 +1,6 @@
import { useTheme } from '@emotion/react'; import { useTheme } from '@emotion/react';
import { Button } from '@/ui/button/components/Button'; import { Button, ButtonVariant } from '@/ui/button/components/Button';
import { ButtonGroup } from '@/ui/button/components/ButtonGroup'; import { ButtonGroup } from '@/ui/button/components/ButtonGroup';
import { IconCheckbox, IconNotes, IconTimelineEvent } from '@/ui/icon/index'; import { IconCheckbox, IconNotes, IconTimelineEvent } from '@/ui/icon/index';
@ -17,7 +17,7 @@ export function CommentThreadCreateButton({
}: CommentThreadCreateButtonProps) { }: CommentThreadCreateButtonProps) {
const theme = useTheme(); const theme = useTheme();
return ( return (
<ButtonGroup variant="secondary"> <ButtonGroup variant={ButtonVariant.Secondary}>
<Button <Button
icon={<IconNotes size={theme.icon.size.sm} />} icon={<IconNotes size={theme.icon.size.sm} />}
title="Note" title="Note"

View File

@ -0,0 +1,190 @@
import React, { useCallback, useState } from 'react';
import { getOperationName } from '@apollo/client/utilities';
import styled from '@emotion/styled';
import { CommentThreadBodyEditor } from '@/activities/components/CommentThreadBodyEditor';
import { CommentThreadComments } from '@/activities/components/CommentThreadComments';
import { CommentThreadRelationPicker } from '@/activities/components/CommentThreadRelationPicker';
import { CommentThreadTypeDropdown } from '@/activities/components/CommentThreadTypeDropdown';
import { GET_COMMENT_THREADS_BY_TARGETS } from '@/activities/queries';
import { PropertyBox } from '@/ui/editable-field/property-box/components/PropertyBox';
import { PropertyBoxItem } from '@/ui/editable-field/property-box/components/PropertyBoxItem';
import { useIsMobile } from '@/ui/hooks/useIsMobile';
import { IconArrowUpRight } from '@/ui/icon/index';
import {
CommentThread,
CommentThreadTarget,
useUpdateCommentThreadMutation,
} from '~/generated/graphql';
import { debounce } from '~/utils/debounce';
import { CommentThreadActionBar } from '../right-drawer/components/CommentThreadActionBar';
import { CommentForDrawer } from '../types/CommentForDrawer';
import '@blocknote/core/style.css';
const StyledContainer = styled.div`
box-sizing: border-box;
display: flex;
flex-direction: column;
height: 100%;
justify-content: space-between;
overflow-y: auto;
`;
const StyledUpperPartContainer = styled.div`
align-items: flex-start;
box-sizing: border-box;
display: flex;
flex-direction: column;
gap: ${({ theme }) => theme.spacing(4)};
justify-content: flex-start;
`;
const StyledTopContainer = styled.div`
align-items: flex-start;
align-self: stretch;
background: ${({ theme }) => theme.background.secondary};
border-bottom: ${({ theme }) =>
useIsMobile() ? 'none' : `1px solid ${theme.border.color.medium}`};
display: flex;
flex-direction: column;
gap: 24px;
padding: 24px 24px 24px 48px;
`;
const StyledEditableTitleInput = styled.input`
background: transparent;
border: none;
color: ${({ theme }) => theme.font.color.primary};
display: flex;
flex: 1 0 0;
flex-direction: column;
font-family: Inter;
font-size: ${({ theme }) => theme.font.size.xl};
font-style: normal;
font-weight: ${({ theme }) => theme.font.weight.semiBold};
justify-content: center;
line-height: ${({ theme }) => theme.text.lineHeight.md};
outline: none;
width: calc(100% - ${({ theme }) => theme.spacing(2)});
&::placeholder {
color: ${({ theme }) => theme.font.color.light};
}
`;
const StyledTopActionsContainer = styled.div`
align-items: center;
display: flex;
flex-direction: row;
justify-content: space-between;
width: 100%;
`;
type OwnProps = {
commentThread: Pick<CommentThread, 'id' | 'title' | 'body' | 'type'> & {
comments?: Array<CommentForDrawer> | null;
} & {
commentThreadTargets?: Array<
Pick<CommentThreadTarget, 'id' | 'commentableId' | 'commentableType'>
> | null;
};
showComment?: boolean;
autoFillTitle?: boolean;
};
export function CommentThreadEditor({
commentThread,
showComment = true,
autoFillTitle = false,
}: OwnProps) {
const [hasUserManuallySetTitle, setHasUserManuallySetTitle] =
useState<boolean>(false);
const [title, setTitle] = useState<string | null>(commentThread.title ?? '');
const [updateCommentThreadMutation] = useUpdateCommentThreadMutation();
const updateTitle = useCallback(
(newTitle: string) => {
updateCommentThreadMutation({
variables: {
id: commentThread.id,
title: newTitle ?? '',
},
refetchQueries: [
getOperationName(GET_COMMENT_THREADS_BY_TARGETS) ?? '',
],
});
},
[commentThread, updateCommentThreadMutation],
);
const debouncedUpdateTitle = debounce(updateTitle, 200);
function updateTitleFromBody(body: string) {
const parsedTitle = JSON.parse(body)[0]?.content[0]?.text;
if (!hasUserManuallySetTitle && autoFillTitle) {
setTitle(parsedTitle);
debouncedUpdateTitle(parsedTitle);
}
}
if (!commentThread) {
return <></>;
}
return (
<StyledContainer>
<StyledUpperPartContainer>
<StyledTopContainer>
<StyledTopActionsContainer>
<CommentThreadTypeDropdown commentThread={commentThread} />
<CommentThreadActionBar commentThreadId={commentThread?.id ?? ''} />
</StyledTopActionsContainer>
<StyledEditableTitleInput
autoComplete="off"
autoFocus
placeholder={`${commentThread.type} title (optional)`}
onChange={(event) => {
setHasUserManuallySetTitle(true);
setTitle(event.target.value);
debouncedUpdateTitle(event.target.value);
}}
value={title ?? ''}
/>
<PropertyBox>
<PropertyBoxItem
icon={<IconArrowUpRight />}
value={
<CommentThreadRelationPicker
commentThread={{
id: commentThread.id,
commentThreadTargets:
commentThread.commentThreadTargets ?? [],
}}
/>
}
label="Relations"
/>
</PropertyBox>
</StyledTopContainer>
<CommentThreadBodyEditor
commentThread={commentThread}
onChange={updateTitleFromBody}
/>
</StyledUpperPartContainer>
{showComment && (
<CommentThreadComments
commentThread={{
id: commentThread.id,
comments: commentThread.comments ?? [],
}}
/>
)}
</StyledContainer>
);
}

View File

@ -1,88 +1,8 @@
import React, { useEffect, useMemo, useState } from 'react'; import { CommentThreadEditor } from '@/activities/components/CommentThreadEditor';
import { getOperationName } from '@apollo/client/utilities'; import { useGetCommentThreadQuery } from '~/generated/graphql';
import styled from '@emotion/styled';
import { CommentThreadBodyEditor } from '@/activities/components/CommentThreadBodyEditor';
import { CommentThreadComments } from '@/activities/components/CommentThreadComments';
import { CommentThreadRelationPicker } from '@/activities/components/CommentThreadRelationPicker';
import { CommentThreadTypeDropdown } from '@/activities/components/CommentThreadTypeDropdown';
import { GET_COMMENT_THREAD } from '@/activities/queries';
import { PropertyBox } from '@/ui/editable-field/property-box/components/PropertyBox';
import { PropertyBoxItem } from '@/ui/editable-field/property-box/components/PropertyBoxItem';
import { useIsMobile } from '@/ui/hooks/useIsMobile';
import { IconArrowUpRight } from '@/ui/icon/index';
import {
useGetCommentThreadQuery,
useUpdateCommentThreadMutation,
} from '~/generated/graphql';
import { debounce } from '~/utils/debounce';
import { CommentThreadActionBar } from './CommentThreadActionBar';
import '@blocknote/core/style.css'; import '@blocknote/core/style.css';
const StyledContainer = styled.div`
box-sizing: border-box;
display: flex;
flex-direction: column;
height: 100%;
justify-content: space-between;
overflow-y: auto;
`;
const StyledUpperPartContainer = styled.div`
align-items: flex-start;
box-sizing: border-box;
display: flex;
flex-direction: column;
gap: ${({ theme }) => theme.spacing(4)};
justify-content: flex-start;
`;
const StyledTopContainer = styled.div`
align-items: flex-start;
align-self: stretch;
background: ${({ theme }) => theme.background.secondary};
border-bottom: ${({ theme }) =>
useIsMobile() ? 'none' : `1px solid ${theme.border.color.medium}`};
display: flex;
flex-direction: column;
gap: 24px;
padding: 24px 24px 24px 48px;
`;
const StyledEditableTitleInput = styled.input`
background: transparent;
border: none;
color: ${({ theme }) => theme.font.color.primary};
display: flex;
flex: 1 0 0;
flex-direction: column;
font-family: Inter;
font-size: ${({ theme }) => theme.font.size.xl};
font-style: normal;
font-weight: ${({ theme }) => theme.font.weight.semiBold};
justify-content: center;
line-height: ${({ theme }) => theme.text.lineHeight.md};
outline: none;
width: calc(100% - ${({ theme }) => theme.spacing(2)});
&::placeholder {
color: ${({ theme }) => theme.font.color.light};
}
`;
const StyledTopActionsContainer = styled.div`
align-items: center;
display: flex;
flex-direction: row;
justify-content: space-between;
width: 100%;
`;
type OwnProps = { type OwnProps = {
commentThreadId: string; commentThreadId: string;
showComment?: boolean; showComment?: boolean;
@ -101,103 +21,14 @@ export function CommentThread({
skip: !commentThreadId, skip: !commentThreadId,
}); });
const commentThread = data?.findManyCommentThreads[0]; const commentThread = data?.findManyCommentThreads[0];
const [hasUserManuallySetTitle, setHasUserManuallySetTitle] =
useState<boolean>(false);
const [title, setTitle] = useState<string | null>(null); return commentThread ? (
<CommentThreadEditor
useEffect(() => { commentThread={commentThread}
if (!hasUserManuallySetTitle) { showComment={showComment}
setTitle(commentThread?.title ?? ''); autoFillTitle={autoFillTitle}
} />
}, [setTitle, commentThread?.title, hasUserManuallySetTitle]); ) : (
<></>
const [updateCommentThreadMutation] = useUpdateCommentThreadMutation();
const debounceUpdateTitle = useMemo(() => {
function updateTitle(title: string) {
if (commentThread) {
updateCommentThreadMutation({
variables: {
id: commentThreadId,
title: title ?? '',
},
refetchQueries: [getOperationName(GET_COMMENT_THREAD) ?? ''],
optimisticResponse: {
__typename: 'Mutation',
updateOneCommentThread: {
__typename: 'CommentThread',
id: commentThreadId,
title: title,
type: commentThread.type,
},
},
});
}
}
return debounce(updateTitle, 200);
}, [commentThreadId, updateCommentThreadMutation, commentThread]);
function updateTitleFromBody(body: string) {
const parsedTitle = JSON.parse(body)[0]?.content[0]?.text;
if (!hasUserManuallySetTitle && autoFillTitle) {
setTitle(parsedTitle);
debounceUpdateTitle(parsedTitle);
}
}
if (!commentThread) {
return <></>;
}
return (
<StyledContainer>
<StyledUpperPartContainer>
<StyledTopContainer>
<StyledTopActionsContainer>
<CommentThreadTypeDropdown commentThread={commentThread} />
<CommentThreadActionBar commentThreadId={commentThread?.id ?? ''} />
</StyledTopActionsContainer>
<StyledEditableTitleInput
autoComplete="off"
autoFocus
placeholder={`${commentThread.type} title (optional)`}
onChange={(event) => {
setHasUserManuallySetTitle(true);
setTitle(event.target.value);
debounceUpdateTitle(event.target.value);
}}
value={title ?? ''}
/>
<PropertyBox>
<PropertyBoxItem
icon={<IconArrowUpRight />}
value={
<CommentThreadRelationPicker
commentThread={{
id: commentThread.id,
commentThreadTargets:
commentThread.commentThreadTargets ?? [],
}}
/>
}
label="Relations"
/>
</PropertyBox>
</StyledTopContainer>
<CommentThreadBodyEditor
commentThread={commentThread}
onChange={updateTitleFromBody}
/>
</StyledUpperPartContainer>
{showComment && (
<CommentThreadComments
commentThread={{
id: commentThread.id,
comments: commentThread.comments ?? [],
}}
/>
)}
</StyledContainer>
); );
} }

View File

@ -6,7 +6,7 @@ import { useRecoilState } from 'recoil';
import { GET_COMMENT_THREADS_BY_TARGETS } from '@/activities/queries'; import { GET_COMMENT_THREADS_BY_TARGETS } from '@/activities/queries';
import { GET_COMPANIES } from '@/companies/queries'; import { GET_COMPANIES } from '@/companies/queries';
import { GET_PEOPLE } from '@/people/queries'; import { GET_PEOPLE } from '@/people/queries';
import { Button } from '@/ui/button/components/Button'; import { Button, ButtonVariant } from '@/ui/button/components/Button';
import { IconTrash } from '@/ui/icon'; import { IconTrash } from '@/ui/icon';
import { isRightDrawerOpenState } from '@/ui/right-drawer/states/isRightDrawerOpenState'; import { isRightDrawerOpenState } from '@/ui/right-drawer/states/isRightDrawerOpenState';
import { useDeleteCommentThreadMutation } from '~/generated/graphql'; import { useDeleteCommentThreadMutation } from '~/generated/graphql';
@ -44,7 +44,7 @@ export function CommentThreadActionBar({ commentThreadId }: OwnProps) {
<IconTrash size={theme.icon.size.sm} stroke={theme.icon.stroke.md} /> <IconTrash size={theme.icon.size.sm} stroke={theme.icon.stroke.md} />
} }
onClick={deleteCommentThread} onClick={deleteCommentThread}
variant="tertiary" variant={ButtonVariant.Tertiary}
/> />
</StyledContainer> </StyledContainer>
); );

View File

@ -1,4 +1,3 @@
import { useEffect, useState } from 'react';
import { useRecoilValue } from 'recoil'; import { useRecoilValue } from 'recoil';
import { companyAddressFamilyState } from '@/companies/states/companyAddressFamilyState'; import { companyAddressFamilyState } from '@/companies/states/companyAddressFamilyState';
@ -15,28 +14,21 @@ export function EditableCompanyAddressCell() {
companyAddressFamilyState(currentRowEntityId ?? ''), companyAddressFamilyState(currentRowEntityId ?? ''),
); );
const [internalValue, setInternalValue] = useState(address ?? '');
useEffect(() => {
setInternalValue(address ?? '');
}, [address]);
return ( return (
<EditableCellText <EditableCellText
value={internalValue} value={address || ''}
onChange={setInternalValue} onSubmit={(newAddress) =>
onSubmit={() =>
updateCompany({ updateCompany({
variables: { variables: {
where: { where: {
id: currentRowEntityId, id: currentRowEntityId,
}, },
data: { data: {
address: internalValue, address: newAddress,
}, },
}, },
}) })
} }
onCancel={() => setInternalValue(address ?? '')}
/> />
); );
} }

View File

@ -1,4 +1,3 @@
import { useEffect, useState } from 'react';
import { useRecoilValue } from 'recoil'; import { useRecoilValue } from 'recoil';
import { companyEmployeesFamilyState } from '@/companies/states/companyEmployeesFamilyState'; import { companyEmployeesFamilyState } from '@/companies/states/companyEmployeesFamilyState';
@ -15,30 +14,22 @@ export function EditableCompanyEmployeesCell() {
companyEmployeesFamilyState(currentRowEntityId ?? ''), companyEmployeesFamilyState(currentRowEntityId ?? ''),
); );
const [internalValue, setInternalValue] = useState(employees ?? '');
useEffect(() => {
setInternalValue(employees ?? '');
}, [employees]);
return ( return (
// TODO: Create an EditableCellNumber component // TODO: Create an EditableCellNumber component
<EditableCellText <EditableCellText
value={internalValue} value={employees || ''}
onChange={setInternalValue} onSubmit={(newValue) =>
onSubmit={() =>
updateCompany({ updateCompany({
variables: { variables: {
where: { where: {
id: currentRowEntityId, id: currentRowEntityId,
}, },
data: { data: {
employees: parseInt(internalValue), employees: parseInt(newValue),
}, },
}, },
}) })
} }
onCancel={() => setInternalValue(employees ?? '')}
/> />
); );
} }

View File

@ -1,4 +1,3 @@
import { useEffect, useState } from 'react';
import { useRecoilValue } from 'recoil'; import { useRecoilValue } from 'recoil';
import { useCurrentRowEntityId } from '@/ui/table/hooks/useCurrentEntityId'; import { useCurrentRowEntityId } from '@/ui/table/hooks/useCurrentEntityId';
@ -15,28 +14,22 @@ export function EditableCompanyLinkedinUrlCell() {
const linkedinUrl = useRecoilValue( const linkedinUrl = useRecoilValue(
companyLinkedinUrlFamilyState(currentRowEntityId ?? ''), companyLinkedinUrlFamilyState(currentRowEntityId ?? ''),
); );
const [internalValue, setInternalValue] = useState(linkedinUrl ?? '');
useEffect(() => {
setInternalValue(linkedinUrl ?? '');
}, [linkedinUrl]);
return ( return (
<EditableCellURL <EditableCellURL
url={internalValue} url={linkedinUrl || ''}
onChange={setInternalValue} onSubmit={(newUrl) =>
onSubmit={() =>
updateCompany({ updateCompany({
variables: { variables: {
where: { where: {
id: currentRowEntityId, id: currentRowEntityId,
}, },
data: { data: {
linkedinUrl: internalValue, linkedinUrl: newUrl,
}, },
}, },
}) })
} }
onCancel={() => setInternalValue(linkedinUrl ?? '')}
/> />
); );
} }

View File

@ -1,4 +1,3 @@
import { useEffect, useState } from 'react';
import { useRecoilValue } from 'recoil'; import { useRecoilValue } from 'recoil';
import { peopleEmailFamilyState } from '@/people/states/peopleEmailFamilyState'; import { peopleEmailFamilyState } from '@/people/states/peopleEmailFamilyState';
@ -15,29 +14,21 @@ export function EditablePeopleEmailCell() {
peopleEmailFamilyState(currentRowEntityId ?? ''), peopleEmailFamilyState(currentRowEntityId ?? ''),
); );
const [internalValue, setInternalValue] = useState(email ?? '');
useEffect(() => {
setInternalValue(email ?? '');
}, [email]);
return ( return (
<EditableCellText <EditableCellText
value={internalValue} value={email || ''}
onChange={setInternalValue} onSubmit={(newEmail: string) =>
onSubmit={() =>
updatePerson({ updatePerson({
variables: { variables: {
where: { where: {
id: currentRowEntityId, id: currentRowEntityId,
}, },
data: { data: {
email: internalValue, email: newEmail,
}, },
}, },
}) })
} }
onCancel={() => setInternalValue(email ?? '')}
/> />
); );
} }

View File

@ -1,16 +1,16 @@
export enum AppPath { export enum AppPath {
// Not logged-in // Not logged-in
Verify = 'verify', Verify = '/verify',
SignIn = 'sign-in', SignIn = '/sign-in',
SignUp = 'sign-up', SignUp = '/sign-up',
Invite = 'invite/:workspaceInviteHash', Invite = '/invite/:workspaceInviteHash',
// Onboarding // Onboarding
CreateWorkspace = 'create/workspace', CreateWorkspace = '/create/workspace',
CreateProfile = 'create/profile', CreateProfile = '/create/profile',
// Onboarded // Onboarded
Index = '', Index = '/',
PeoplePage = '/people', PeoplePage = '/people',
CompaniesPage = '/companies', CompaniesPage = '/companies',
CompanyShowPage = '/companies/:companyId', CompanyShowPage = '/companies/:companyId',

View File

@ -19,11 +19,11 @@ export function ButtonGroup({ children, variant, size }: ButtonGroupProps) {
let position: ButtonPosition; let position: ButtonPosition;
if (index === 0) { if (index === 0) {
position = 'left'; position = ButtonPosition.Left;
} else if (index === children.length - 1) { } else if (index === children.length - 1) {
position = 'right'; position = ButtonPosition.Right;
} else { } else {
position = 'middle'; position = ButtonPosition.Middle;
} }
const additionalProps: any = { position }; const additionalProps: any = { position };

View File

@ -48,6 +48,7 @@ export function InplaceInputTextEditMode({
return ( return (
<StyledInput <StyledInput
autoComplete="off"
ref={wrapperRef} ref={wrapperRef}
placeholder={placeholder} placeholder={placeholder}
onChange={handleChange} onChange={handleChange}

View File

@ -2,7 +2,7 @@ import React from 'react';
import { useTheme } from '@emotion/react'; import { useTheme } from '@emotion/react';
import styled from '@emotion/styled'; import styled from '@emotion/styled';
import { Button } from '@/ui/button/components/Button'; import { Button, ButtonVariant } from '@/ui/button/components/Button';
import { IconFileUpload, IconTrash, IconUpload } from '@/ui/icon'; import { IconFileUpload, IconTrash, IconUpload } from '@/ui/icon';
const Container = styled.div` const Container = styled.div`
@ -123,7 +123,7 @@ export function ImageInput({
<Button <Button
icon={<IconUpload size={theme.icon.size.sm} />} icon={<IconUpload size={theme.icon.size.sm} />}
onClick={onUploadButtonClick} onClick={onUploadButtonClick}
variant="secondary" variant={ButtonVariant.Secondary}
title="Upload" title="Upload"
disabled={disabled} disabled={disabled}
fullWidth fullWidth
@ -131,7 +131,7 @@ export function ImageInput({
<Button <Button
icon={<IconTrash size={theme.icon.size.sm} />} icon={<IconTrash size={theme.icon.size.sm} />}
onClick={onRemove} onClick={onRemove}
variant="secondary" variant={ButtonVariant.Secondary}
title="Remove" title="Remove"
disabled={!picture || disabled} disabled={!picture || disabled}
fullWidth fullWidth

View File

@ -48,9 +48,9 @@ export function EditableCellDoubleTextEditMode({
setSecondInternalValue(secondValue); setSecondInternalValue(secondValue);
}, [firstValue, secondValue]); }, [firstValue, secondValue]);
function handleOnChange(firstValue: string, secondValue: string): void { function handleOnChange(newFirstValue: string, newSecondValue: string): void {
setFirstInternalValue(firstValue); setFirstInternalValue(newFirstValue);
setSecondInternalValue(secondValue); setSecondInternalValue(newSecondValue);
} }
const [focusPosition, setFocusPosition] = useState<'left' | 'right'>('left'); const [focusPosition, setFocusPosition] = useState<'left' | 'right'>('left');
@ -142,18 +142,18 @@ export function EditableCellDoubleTextEditMode({
autoFocus autoFocus
placeholder={firstValuePlaceholder} placeholder={firstValuePlaceholder}
ref={firstValueInputRef} ref={firstValueInputRef}
value={firstValue} value={firstInternalValue}
onChange={(event: ChangeEvent<HTMLInputElement>) => { onChange={(event: ChangeEvent<HTMLInputElement>) => {
handleOnChange(event.target.value, secondValue); handleOnChange(event.target.value, secondInternalValue);
}} }}
/> />
<StyledInput <StyledInput
autoComplete="off" autoComplete="off"
placeholder={secondValuePlaceholder} placeholder={secondValuePlaceholder}
ref={secondValueInputRef} ref={secondValueInputRef}
value={secondValue} value={secondInternalValue}
onChange={(event: ChangeEvent<HTMLInputElement>) => { onChange={(event: ChangeEvent<HTMLInputElement>) => {
handleOnChange(firstValue, event.target.value); handleOnChange(firstInternalValue, event.target.value);
}} }}
/> />
</StyledContainer> </StyledContainer>

View File

@ -14,7 +14,6 @@ export function EditableCellPhone({ value, placeholder, onSubmit }: OwnProps) {
<EditableCell <EditableCell
editModeContent={ editModeContent={
<InplaceInputTextEditMode <InplaceInputTextEditMode
autoComplete="off"
autoFocus autoFocus
placeholder={placeholder || ''} placeholder={placeholder || ''}
value={value} value={value}

View File

@ -32,7 +32,6 @@ export function EditableCellText({
autoFocus autoFocus
value={value} value={value}
onSubmit={(newText) => onSubmit?.(newText)} onSubmit={(newText) => onSubmit?.(newText)}
autoComplete="off"
/> />
} }
nonEditModeContent={ nonEditModeContent={

View File

@ -27,7 +27,6 @@ export function EditableCellURL({
editModeContent={ editModeContent={
<InplaceInputTextEditMode <InplaceInputTextEditMode
placeholder={placeholder} placeholder={placeholder}
autoComplete="off"
autoFocus autoFocus
value={url} value={url}
onSubmit={(newURL) => onSubmit?.(newURL)} onSubmit={(newURL) => onSubmit?.(newURL)}

View File

@ -56,7 +56,6 @@ export function EditableCellChip({
placeholder={placeholder || ''} placeholder={placeholder || ''}
autoFocus autoFocus
value={inputValue} value={inputValue}
autoComplete="off"
onSubmit={(newValue) => onSubmit?.(newValue)} onSubmit={(newValue) => onSubmit?.(newValue)}
/> />
} }

View File

@ -1,7 +1,7 @@
import { useTheme } from '@emotion/react'; import { useTheme } from '@emotion/react';
import styled from '@emotion/styled'; import styled from '@emotion/styled';
import { Button } from '@/ui/button/components/Button'; import { Button, ButtonVariant } from '@/ui/button/components/Button';
import { IconCopy, IconLink } from '@/ui/icon'; import { IconCopy, IconLink } from '@/ui/icon';
import { TextInput } from '@/ui/input/components/TextInput'; import { TextInput } from '@/ui/input/components/TextInput';
import { useSnackBar } from '@/ui/snack-bar/hooks/useSnackBar'; import { useSnackBar } from '@/ui/snack-bar/hooks/useSnackBar';
@ -33,7 +33,7 @@ export function WorkspaceInviteLink({ inviteLink }: OwnProps) {
</StyledLinkContainer> </StyledLinkContainer>
<Button <Button
icon={<IconLink size={theme.icon.size.md} />} icon={<IconLink size={theme.icon.size.md} />}
variant="primary" variant={ButtonVariant.Primary}
title="Copy link" title="Copy link"
onClick={() => { onClick={() => {
enqueueSnackBar('Link copied to clipboard', { enqueueSnackBar('Link copied to clipboard', {

View File

@ -5,6 +5,7 @@ import { useAuth } from '@/auth/hooks/useAuth';
import { useIsLogged } from '@/auth/hooks/useIsLogged'; import { useIsLogged } from '@/auth/hooks/useIsLogged';
import { AppPath } from '../../modules/types/AppPath'; import { AppPath } from '../../modules/types/AppPath';
import { isNonEmptyString } from '../../utils/isNonEmptyString';
export function Verify() { export function Verify() {
const [searchParams] = useSearchParams(); const [searchParams] = useSearchParams();
@ -20,8 +21,17 @@ export function Verify() {
if (!loginToken) { if (!loginToken) {
navigate(AppPath.SignIn); navigate(AppPath.SignIn);
} else { } else {
await verify(loginToken); const verifyResponse = await verify(loginToken);
navigate(AppPath.CompaniesPage);
if (
isNonEmptyString(
verifyResponse.user.workspaceMember?.workspace.displayName,
)
) {
navigate(AppPath.Index);
} else {
navigate(AppPath.CreateWorkspace);
}
} }
} }

View File

@ -3,7 +3,11 @@ import styled from '@emotion/styled';
import { useRecoilState } from 'recoil'; import { useRecoilState } from 'recoil';
import { currentUserState } from '@/auth/states/currentUserState'; import { currentUserState } from '@/auth/states/currentUserState';
import { Button } from '@/ui/button/components/Button'; import {
Button,
ButtonSize,
ButtonVariant,
} from '@/ui/button/components/Button';
import { IconSettings, IconTrash } from '@/ui/icon'; import { IconSettings, IconTrash } from '@/ui/icon';
import { SubMenuTopBarContainer } from '@/ui/layout/components/SubMenuTopBarContainer'; import { SubMenuTopBarContainer } from '@/ui/layout/components/SubMenuTopBarContainer';
import { MainSectionTitle } from '@/ui/title/components/MainSectionTitle'; import { MainSectionTitle } from '@/ui/title/components/MainSectionTitle';
@ -102,8 +106,8 @@ export function SettingsWorkspaceMembers() {
<ButtonContainer> <ButtonContainer>
<Button <Button
onClick={() => handleRemoveWorkspaceMember(member.user.id)} onClick={() => handleRemoveWorkspaceMember(member.user.id)}
variant="tertiary" variant={ButtonVariant.Tertiary}
size="small" size={ButtonSize.Small}
icon={<IconTrash size={theme.icon.size.md} />} icon={<IconTrash size={theme.icon.size.md} />}
/> />
</ButtonContainer> </ButtonContainer>

View File

@ -9,7 +9,7 @@
"prebuild": "rimraf dist", "prebuild": "rimraf dist",
"build": "nest build", "build": "nest build",
"format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"", "format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"",
"start": "set NODE_ENV=development&& nest start", "start": "set NODE_ENV=development && nest start",
"start:dev": "nest start --watch", "start:dev": "nest start --watch",
"start:debug": "nest start --debug --watch", "start:debug": "nest start --debug --watch",
"start:prod": "node dist/main", "start:prod": "node dist/main",
@ -32,7 +32,7 @@
"@aws-sdk/client-s3": "^3.363.0", "@aws-sdk/client-s3": "^3.363.0",
"@aws-sdk/credential-providers": "^3.363.0", "@aws-sdk/credential-providers": "^3.363.0",
"@casl/ability": "^6.5.0", "@casl/ability": "^6.5.0",
"@casl/prisma": "^1.4.0", "@casl/prisma": "1.4.0",
"@nestjs/apollo": "^11.0.5", "@nestjs/apollo": "^11.0.5",
"@nestjs/common": "^9.0.0", "@nestjs/common": "^9.0.0",
"@nestjs/config": "^2.3.2", "@nestjs/config": "^2.3.2",
@ -44,7 +44,7 @@
"@nestjs/serve-static": "^3.0.0", "@nestjs/serve-static": "^3.0.0",
"@nestjs/terminus": "^9.2.2", "@nestjs/terminus": "^9.2.2",
"@paljs/plugins": "^5.3.3", "@paljs/plugins": "^5.3.3",
"@prisma/client": "^4.13.0", "@prisma/client": "4.13.0",
"@types/lodash.camelcase": "^4.3.7", "@types/lodash.camelcase": "^4.3.7",
"@types/lodash.merge": "^4.6.7", "@types/lodash.merge": "^4.6.7",
"add": "^2.0.6", "add": "^2.0.6",
@ -101,7 +101,7 @@
"eslint-plugin-prettier": "^4.0.0", "eslint-plugin-prettier": "^4.0.0",
"jest": "28.1.3", "jest": "28.1.3",
"prettier": "^2.3.2", "prettier": "^2.3.2",
"prisma": "^4.13.0", "prisma": "4.13.0",
"prisma-nestjs-graphql": "^18.0.2", "prisma-nestjs-graphql": "^18.0.2",
"prisma-query-log": "^3.2.0", "prisma-query-log": "^3.2.0",
"source-map-support": "^0.5.20", "source-map-support": "^0.5.20",

View File

@ -16,6 +16,7 @@ import {
PipelineStage, PipelineStage,
PipelineProgress, PipelineProgress,
Attachment, Attachment,
UserSettings,
} from '@prisma/client'; } from '@prisma/client';
import { AbilityAction } from './ability.action'; import { AbilityAction } from './ability.action';
@ -34,6 +35,7 @@ type SubjectsAbility = Subjects<{
PipelineStage: PipelineStage; PipelineStage: PipelineStage;
PipelineProgress: PipelineProgress; PipelineProgress: PipelineProgress;
Attachment: Attachment; Attachment: Attachment;
UserSettings: UserSettings;
}>; }>;
export type AppAbility = PureAbility< export type AppAbility = PureAbility<
@ -58,8 +60,9 @@ export class AbilityFactory {
cannot(AbilityAction.Delete, 'User'); cannot(AbilityAction.Delete, 'User');
// Workspace // Workspace
can(AbilityAction.Read, 'Workspace', { id: workspace.id }); can(AbilityAction.Read, 'Workspace');
can(AbilityAction.Update, 'Workspace', { id: workspace.id }); can(AbilityAction.Update, 'Workspace');
can(AbilityAction.Delete, 'Workspace');
// Workspace Member // Workspace Member
can(AbilityAction.Read, 'WorkspaceMember', { workspaceId: workspace.id }); can(AbilityAction.Read, 'WorkspaceMember', { workspaceId: workspace.id });
@ -101,6 +104,7 @@ export class AbilityFactory {
// CommentThreadTarget // CommentThreadTarget
can(AbilityAction.Read, 'CommentThreadTarget'); can(AbilityAction.Read, 'CommentThreadTarget');
can(AbilityAction.Create, 'CommentThreadTarget');
// Attachment // Attachment
can(AbilityAction.Read, 'Attachment', { workspaceId: workspace.id }); can(AbilityAction.Read, 'Attachment', { workspaceId: workspace.id });

View File

@ -0,0 +1,207 @@
import { Prisma, PrismaClient } from '@prisma/client';
import { subject } from '@casl/ability';
import { camelCase } from 'src/utils/camel-case';
import { AppAbility } from './ability.factory';
import { AbilityAction } from './ability.action';
type OperationType =
| 'create'
| 'connectOrCreate'
| 'upsert'
| 'createMany'
| 'set'
| 'disconnect'
| 'delete'
| 'connect'
| 'update'
| 'updateMany'
| 'deleteMany';
// in most case unique identifier is the id, but it can be something else...
type OperationAbilityChecker = (
modelName: Prisma.ModelName,
ability: AppAbility,
prisma: PrismaClient,
data: any,
) => Promise<boolean>;
const createAbilityCheck: OperationAbilityChecker = async (
modelName,
ability,
prisma,
data,
) => {
// Handle all operations cases
const items = data?.data
? !Array.isArray(data.data)
? [data.data]
: data.data
: !Array.isArray(data)
? [data]
: data;
// Check if user try to create an element that is not allowed to create
for (const {} of items) {
if (!ability.can(AbilityAction.Create, modelName)) {
return false;
}
}
return true;
};
const simpleAbilityCheck: OperationAbilityChecker = async (
modelName,
ability,
prisma,
data,
) => {
// Extract entity name from model name
const entity = camelCase(modelName);
// Handle all operations cases
const operations = !Array.isArray(data) ? [data] : data;
// Handle where case
const normalizedOperations = operations.map((op) =>
op.where ? op.where : op,
);
// Force entity type because of Prisma typing
const items = await prisma[entity as string].findMany({
where: {
OR: normalizedOperations,
},
});
// Check if user try to connect an element that is not allowed to read
for (const item of items) {
// TODO: Replace user by workspaceMember and remove this check
if (
modelName === 'User' ||
modelName === 'UserSettings' ||
modelName === 'Workspace'
) {
return true;
}
if (!ability.can(AbilityAction.Read, subject(modelName, item))) {
return false;
}
}
return true;
};
const operationAbilityCheckers: Record<OperationType, OperationAbilityChecker> =
{
create: createAbilityCheck,
createMany: createAbilityCheck,
upsert: simpleAbilityCheck,
update: simpleAbilityCheck,
updateMany: simpleAbilityCheck,
delete: simpleAbilityCheck,
deleteMany: simpleAbilityCheck,
connectOrCreate: simpleAbilityCheck,
connect: simpleAbilityCheck,
disconnect: simpleAbilityCheck,
set: simpleAbilityCheck,
};
// Check relation nested abilities
export async function relationAbilityChecker(
modelName: Prisma.ModelName,
ability: AppAbility,
prisma: PrismaClient,
args: any,
) {
// Extract models from Prisma
const models = Prisma.dmmf.datamodel.models;
// Find main model from options
const mainModel = models.find((item) => item.name === modelName);
if (!mainModel) {
throw new Error('Main model not found');
}
// Loop over fields
for (const field of mainModel.fields) {
// Check if field is a relation
if (field.relationName) {
// Check if field is in args
const operation = args.data?.[field.name] ?? args?.[field.name];
if (operation) {
// Extract operation name and value
const operationType = Object.keys(operation)[0] as OperationType;
const operationValue = operation[operationType];
// Get operation checker for the operation type
const operationChecker = operationAbilityCheckers[operationType];
if (!operationChecker) {
throw new Error('Operation not found');
}
// Check if operation is allowed
const allowed = await operationChecker(
field.type as Prisma.ModelName,
ability,
prisma,
operationValue,
);
if (!allowed) {
return false;
}
// For the 'create', 'connectOrCreate', 'upsert', 'update', and 'updateMany' operations,
// we should also check the nested operations.
if (
[
'create',
'connectOrCreate',
'upsert',
'update',
'updateMany',
].includes(operationType)
) {
// Handle nested operations all cases
const operationValues = !Array.isArray(operationValue)
? [operationValue]
: operationValue;
// Loop over nested args
for (const nestedArgs of operationValues) {
const nestedCreateAllowed = await relationAbilityChecker(
field.type as Prisma.ModelName,
ability,
prisma,
nestedArgs.create ?? nestedArgs.data ?? nestedArgs,
);
if (!nestedCreateAllowed) {
return false;
}
if (nestedArgs.update) {
const nestedUpdateAllowed = await relationAbilityChecker(
field.type as Prisma.ModelName,
ability,
prisma,
nestedArgs.update,
);
if (!nestedUpdateAllowed) {
return false;
}
}
}
}
}
}
}
return true;
}

View File

@ -42,7 +42,7 @@ export class CreateAttachmentAbilityHandler implements IAbilityHandler {
const args = gqlContext.getArgs<AttachmentArgs>(); const args = gqlContext.getArgs<AttachmentArgs>();
assert(args.activityId, '', ForbiddenException); assert(args.activityId, '', ForbiddenException);
const activity = await this.prismaService.commentThread.findUnique({ const activity = await this.prismaService.client.commentThread.findUnique({
where: { id: args.activityId }, where: { id: args.activityId },
include: { workspace: true }, include: { workspace: true },
}); });

View File

@ -13,10 +13,12 @@ import { PrismaService } from 'src/database/prisma.service';
import { AbilityAction } from 'src/ability/ability.action'; import { AbilityAction } from 'src/ability/ability.action';
import { AppAbility } from 'src/ability/ability.factory'; import { AppAbility } from 'src/ability/ability.factory';
import { CommentThreadTargetWhereInput } from 'src/core/@generated/comment-thread-target/comment-thread-target-where.input'; import { CommentThreadTargetWhereInput } from 'src/core/@generated/comment-thread-target/comment-thread-target-where.input';
import { relationAbilityChecker } from 'src/ability/ability.util';
import { assert } from 'src/utils/assert'; import { assert } from 'src/utils/assert';
class CommentThreadTargetArgs { class CommentThreadTargetArgs {
where?: CommentThreadTargetWhereInput; where?: CommentThreadTargetWhereInput;
[key: string]: any;
} }
@Injectable() @Injectable()
@ -39,7 +41,23 @@ export class ReadCommentThreadTargetAbilityHandler implements IAbilityHandler {
export class CreateCommentThreadTargetAbilityHandler export class CreateCommentThreadTargetAbilityHandler
implements IAbilityHandler implements IAbilityHandler
{ {
handle(ability: AppAbility) { constructor(private readonly prismaService: PrismaService) {}
async handle(ability: AppAbility, context: ExecutionContext) {
const gqlContext = GqlExecutionContext.create(context);
const args = gqlContext.getArgs();
const allowed = await relationAbilityChecker(
'CommentThreadTarget',
ability,
this.prismaService.client,
args,
);
if (!allowed) {
return false;
}
return ability.can(AbilityAction.Create, 'CommentThreadTarget'); return ability.can(AbilityAction.Create, 'CommentThreadTarget');
} }
} }
@ -54,11 +72,22 @@ export class UpdateCommentThreadTargetAbilityHandler
const gqlContext = GqlExecutionContext.create(context); const gqlContext = GqlExecutionContext.create(context);
const args = gqlContext.getArgs<CommentThreadTargetArgs>(); const args = gqlContext.getArgs<CommentThreadTargetArgs>();
const commentThreadTarget = const commentThreadTarget =
await this.prismaService.commentThreadTarget.findFirst({ await this.prismaService.client.commentThreadTarget.findFirst({
where: args.where, where: args.where,
}); });
assert(commentThreadTarget, '', NotFoundException); assert(commentThreadTarget, '', NotFoundException);
const allowed = await relationAbilityChecker(
'CommentThreadTarget',
ability,
this.prismaService.client,
args,
);
if (!allowed) {
return false;
}
return ability.can( return ability.can(
AbilityAction.Update, AbilityAction.Update,
subject('CommentThreadTarget', commentThreadTarget), subject('CommentThreadTarget', commentThreadTarget),
@ -76,7 +105,7 @@ export class DeleteCommentThreadTargetAbilityHandler
const gqlContext = GqlExecutionContext.create(context); const gqlContext = GqlExecutionContext.create(context);
const args = gqlContext.getArgs<CommentThreadTargetArgs>(); const args = gqlContext.getArgs<CommentThreadTargetArgs>();
const commentThreadTarget = const commentThreadTarget =
await this.prismaService.commentThreadTarget.findFirst({ await this.prismaService.client.commentThreadTarget.findFirst({
where: args.where, where: args.where,
}); });
assert(commentThreadTarget, '', NotFoundException); assert(commentThreadTarget, '', NotFoundException);

View File

@ -13,10 +13,12 @@ import { PrismaService } from 'src/database/prisma.service';
import { AbilityAction } from 'src/ability/ability.action'; import { AbilityAction } from 'src/ability/ability.action';
import { AppAbility } from 'src/ability/ability.factory'; import { AppAbility } from 'src/ability/ability.factory';
import { CommentThreadWhereInput } from 'src/core/@generated/comment-thread/comment-thread-where.input'; import { CommentThreadWhereInput } from 'src/core/@generated/comment-thread/comment-thread-where.input';
import { relationAbilityChecker } from 'src/ability/ability.util';
import { assert } from 'src/utils/assert'; import { assert } from 'src/utils/assert';
class CommentThreadArgs { class CommentThreadArgs {
where?: CommentThreadWhereInput; where?: CommentThreadWhereInput;
[key: string]: any;
} }
@Injectable() @Injectable()
@ -35,7 +37,23 @@ export class ReadCommentThreadAbilityHandler implements IAbilityHandler {
@Injectable() @Injectable()
export class CreateCommentThreadAbilityHandler implements IAbilityHandler { export class CreateCommentThreadAbilityHandler implements IAbilityHandler {
handle(ability: AppAbility) { constructor(private readonly prismaService: PrismaService) {}
async handle(ability: AppAbility, context: ExecutionContext) {
const gqlContext = GqlExecutionContext.create(context);
const args = gqlContext.getArgs();
const allowed = await relationAbilityChecker(
'CommentThread',
ability,
this.prismaService.client,
args,
);
if (!allowed) {
return false;
}
return ability.can(AbilityAction.Create, 'CommentThread'); return ability.can(AbilityAction.Create, 'CommentThread');
} }
} }
@ -47,11 +65,23 @@ export class UpdateCommentThreadAbilityHandler implements IAbilityHandler {
async handle(ability: AppAbility, context: ExecutionContext) { async handle(ability: AppAbility, context: ExecutionContext) {
const gqlContext = GqlExecutionContext.create(context); const gqlContext = GqlExecutionContext.create(context);
const args = gqlContext.getArgs<CommentThreadArgs>(); const args = gqlContext.getArgs<CommentThreadArgs>();
const commentThread = await this.prismaService.commentThread.findFirst({ const commentThread =
where: args.where, await this.prismaService.client.commentThread.findFirst({
}); where: args.where,
});
assert(commentThread, '', NotFoundException); assert(commentThread, '', NotFoundException);
const allowed = await relationAbilityChecker(
'CommentThread',
ability,
this.prismaService.client,
args,
);
if (!allowed) {
return false;
}
return ability.can( return ability.can(
AbilityAction.Update, AbilityAction.Update,
subject('CommentThread', commentThread), subject('CommentThread', commentThread),
@ -66,9 +96,10 @@ export class DeleteCommentThreadAbilityHandler implements IAbilityHandler {
async handle(ability: AppAbility, context: ExecutionContext) { async handle(ability: AppAbility, context: ExecutionContext) {
const gqlContext = GqlExecutionContext.create(context); const gqlContext = GqlExecutionContext.create(context);
const args = gqlContext.getArgs<CommentThreadArgs>(); const args = gqlContext.getArgs<CommentThreadArgs>();
const commentThread = await this.prismaService.commentThread.findFirst({ const commentThread =
where: args.where, await this.prismaService.client.commentThread.findFirst({
}); where: args.where,
});
assert(commentThread, '', NotFoundException); assert(commentThread, '', NotFoundException);
return ability.can( return ability.can(

View File

@ -13,10 +13,12 @@ import { PrismaService } from 'src/database/prisma.service';
import { AbilityAction } from 'src/ability/ability.action'; import { AbilityAction } from 'src/ability/ability.action';
import { AppAbility } from 'src/ability/ability.factory'; import { AppAbility } from 'src/ability/ability.factory';
import { CommentWhereInput } from 'src/core/@generated/comment/comment-where.input'; import { CommentWhereInput } from 'src/core/@generated/comment/comment-where.input';
import { relationAbilityChecker } from 'src/ability/ability.util';
import { assert } from 'src/utils/assert'; import { assert } from 'src/utils/assert';
class CommentArgs { class CommentArgs {
where?: CommentWhereInput; where?: CommentWhereInput;
[key: string]: any;
} }
@Injectable() @Injectable()
@ -35,7 +37,23 @@ export class ReadCommentAbilityHandler implements IAbilityHandler {
@Injectable() @Injectable()
export class CreateCommentAbilityHandler implements IAbilityHandler { export class CreateCommentAbilityHandler implements IAbilityHandler {
handle(ability: AppAbility) { constructor(private readonly prismaService: PrismaService) {}
async handle(ability: AppAbility, context: ExecutionContext) {
const gqlContext = GqlExecutionContext.create(context);
const args = gqlContext.getArgs();
const allowed = await relationAbilityChecker(
'Comment',
ability,
this.prismaService.client,
args,
);
if (!allowed) {
return false;
}
return ability.can(AbilityAction.Create, 'Comment'); return ability.can(AbilityAction.Create, 'Comment');
} }
} }
@ -47,11 +65,22 @@ export class UpdateCommentAbilityHandler implements IAbilityHandler {
async handle(ability: AppAbility, context: ExecutionContext) { async handle(ability: AppAbility, context: ExecutionContext) {
const gqlContext = GqlExecutionContext.create(context); const gqlContext = GqlExecutionContext.create(context);
const args = gqlContext.getArgs<CommentArgs>(); const args = gqlContext.getArgs<CommentArgs>();
const comment = await this.prismaService.comment.findFirst({ const comment = await this.prismaService.client.comment.findFirst({
where: args.where, where: args.where,
}); });
assert(comment, '', NotFoundException); assert(comment, '', NotFoundException);
const allowed = await relationAbilityChecker(
'Comment',
ability,
this.prismaService.client,
args,
);
if (!allowed) {
return false;
}
return ability.can(AbilityAction.Update, subject('Comment', comment)); return ability.can(AbilityAction.Update, subject('Comment', comment));
} }
} }
@ -63,7 +92,7 @@ export class DeleteCommentAbilityHandler implements IAbilityHandler {
async handle(ability: AppAbility, context: ExecutionContext) { async handle(ability: AppAbility, context: ExecutionContext) {
const gqlContext = GqlExecutionContext.create(context); const gqlContext = GqlExecutionContext.create(context);
const args = gqlContext.getArgs<CommentArgs>(); const args = gqlContext.getArgs<CommentArgs>();
const comment = await this.prismaService.comment.findFirst({ const comment = await this.prismaService.client.comment.findFirst({
where: args.where, where: args.where,
}); });
assert(comment, '', NotFoundException); assert(comment, '', NotFoundException);

View File

@ -13,10 +13,12 @@ import { PrismaService } from 'src/database/prisma.service';
import { AbilityAction } from 'src/ability/ability.action'; import { AbilityAction } from 'src/ability/ability.action';
import { AppAbility } from 'src/ability/ability.factory'; import { AppAbility } from 'src/ability/ability.factory';
import { CompanyWhereInput } from 'src/core/@generated/company/company-where.input'; import { CompanyWhereInput } from 'src/core/@generated/company/company-where.input';
import { relationAbilityChecker } from 'src/ability/ability.util';
import { assert } from 'src/utils/assert'; import { assert } from 'src/utils/assert';
class CompanyArgs { class CompanyArgs {
where?: CompanyWhereInput; where?: CompanyWhereInput;
[key: string]: any;
} }
@Injectable() @Injectable()
@ -35,7 +37,23 @@ export class ReadCompanyAbilityHandler implements IAbilityHandler {
@Injectable() @Injectable()
export class CreateCompanyAbilityHandler implements IAbilityHandler { export class CreateCompanyAbilityHandler implements IAbilityHandler {
handle(ability: AppAbility) { constructor(private readonly prismaService: PrismaService) {}
async handle(ability: AppAbility, context: ExecutionContext) {
const gqlContext = GqlExecutionContext.create(context);
const args = gqlContext.getArgs();
const allowed = await relationAbilityChecker(
'Company',
ability,
this.prismaService.client,
args,
);
if (!allowed) {
return false;
}
return ability.can(AbilityAction.Create, 'Company'); return ability.can(AbilityAction.Create, 'Company');
} }
} }
@ -47,12 +65,22 @@ export class UpdateCompanyAbilityHandler implements IAbilityHandler {
async handle(ability: AppAbility, context: ExecutionContext) { async handle(ability: AppAbility, context: ExecutionContext) {
const gqlContext = GqlExecutionContext.create(context); const gqlContext = GqlExecutionContext.create(context);
const args = gqlContext.getArgs<CompanyArgs>(); const args = gqlContext.getArgs<CompanyArgs>();
const company = await this.prismaService.company.findFirst({ const company = await this.prismaService.client.company.findFirst({
where: args.where, where: args.where,
}); });
assert(company, '', NotFoundException); assert(company, '', NotFoundException);
const allowed = await relationAbilityChecker(
'Company',
ability,
this.prismaService.client,
args,
);
if (!allowed) {
return false;
}
return ability.can(AbilityAction.Update, subject('Company', company)); return ability.can(AbilityAction.Update, subject('Company', company));
} }
} }
@ -64,7 +92,7 @@ export class DeleteCompanyAbilityHandler implements IAbilityHandler {
async handle(ability: AppAbility, context: ExecutionContext) { async handle(ability: AppAbility, context: ExecutionContext) {
const gqlContext = GqlExecutionContext.create(context); const gqlContext = GqlExecutionContext.create(context);
const args = gqlContext.getArgs<CompanyArgs>(); const args = gqlContext.getArgs<CompanyArgs>();
const company = await this.prismaService.company.findFirst({ const company = await this.prismaService.client.company.findFirst({
where: args.where, where: args.where,
}); });
assert(company, '', NotFoundException); assert(company, '', NotFoundException);

View File

@ -13,10 +13,12 @@ import { PrismaService } from 'src/database/prisma.service';
import { AbilityAction } from 'src/ability/ability.action'; import { AbilityAction } from 'src/ability/ability.action';
import { AppAbility } from 'src/ability/ability.factory'; import { AppAbility } from 'src/ability/ability.factory';
import { PersonWhereInput } from 'src/core/@generated/person/person-where.input'; import { PersonWhereInput } from 'src/core/@generated/person/person-where.input';
import { relationAbilityChecker } from 'src/ability/ability.util';
import { assert } from 'src/utils/assert'; import { assert } from 'src/utils/assert';
class PersonArgs { class PersonArgs {
where?: PersonWhereInput; where?: PersonWhereInput;
[key: string]: any;
} }
@Injectable() @Injectable()
@ -35,7 +37,23 @@ export class ReadPersonAbilityHandler implements IAbilityHandler {
@Injectable() @Injectable()
export class CreatePersonAbilityHandler implements IAbilityHandler { export class CreatePersonAbilityHandler implements IAbilityHandler {
handle(ability: AppAbility) { constructor(private readonly prismaService: PrismaService) {}
async handle(ability: AppAbility, context: ExecutionContext) {
const gqlContext = GqlExecutionContext.create(context);
const args = gqlContext.getArgs();
const allowed = await relationAbilityChecker(
'Person',
ability,
this.prismaService.client,
args,
);
if (!allowed) {
return false;
}
return ability.can(AbilityAction.Create, 'Person'); return ability.can(AbilityAction.Create, 'Person');
} }
} }
@ -47,11 +65,22 @@ export class UpdatePersonAbilityHandler implements IAbilityHandler {
async handle(ability: AppAbility, context: ExecutionContext) { async handle(ability: AppAbility, context: ExecutionContext) {
const gqlContext = GqlExecutionContext.create(context); const gqlContext = GqlExecutionContext.create(context);
const args = gqlContext.getArgs<PersonArgs>(); const args = gqlContext.getArgs<PersonArgs>();
const person = await this.prismaService.person.findFirst({ const person = await this.prismaService.client.person.findFirst({
where: args.where, where: args.where,
}); });
assert(person, '', NotFoundException); assert(person, '', NotFoundException);
const allowed = await relationAbilityChecker(
'Person',
ability,
this.prismaService.client,
args,
);
if (!allowed) {
return false;
}
return ability.can(AbilityAction.Update, subject('Person', person)); return ability.can(AbilityAction.Update, subject('Person', person));
} }
} }
@ -63,7 +92,7 @@ export class DeletePersonAbilityHandler implements IAbilityHandler {
async handle(ability: AppAbility, context: ExecutionContext) { async handle(ability: AppAbility, context: ExecutionContext) {
const gqlContext = GqlExecutionContext.create(context); const gqlContext = GqlExecutionContext.create(context);
const args = gqlContext.getArgs<PersonArgs>(); const args = gqlContext.getArgs<PersonArgs>();
const person = await this.prismaService.person.findFirst({ const person = await this.prismaService.client.person.findFirst({
where: args.where, where: args.where,
}); });
assert(person, '', NotFoundException); assert(person, '', NotFoundException);

View File

@ -12,11 +12,13 @@ import { IAbilityHandler } from 'src/ability/interfaces/ability-handler.interfac
import { PrismaService } from 'src/database/prisma.service'; import { PrismaService } from 'src/database/prisma.service';
import { AbilityAction } from 'src/ability/ability.action'; import { AbilityAction } from 'src/ability/ability.action';
import { AppAbility } from 'src/ability/ability.factory'; import { AppAbility } from 'src/ability/ability.factory';
import { assert } from 'src/utils/assert';
import { PipelineProgressWhereInput } from 'src/core/@generated/pipeline-progress/pipeline-progress-where.input'; import { PipelineProgressWhereInput } from 'src/core/@generated/pipeline-progress/pipeline-progress-where.input';
import { relationAbilityChecker } from 'src/ability/ability.util';
import { assert } from 'src/utils/assert';
class PipelineProgressArgs { class PipelineProgressArgs {
where?: PipelineProgressWhereInput; where?: PipelineProgressWhereInput;
[key: string]: any;
} }
@Injectable() @Injectable()
@ -35,7 +37,23 @@ export class ReadPipelineProgressAbilityHandler implements IAbilityHandler {
@Injectable() @Injectable()
export class CreatePipelineProgressAbilityHandler implements IAbilityHandler { export class CreatePipelineProgressAbilityHandler implements IAbilityHandler {
handle(ability: AppAbility) { constructor(private readonly prismaService: PrismaService) {}
async handle(ability: AppAbility, context: ExecutionContext) {
const gqlContext = GqlExecutionContext.create(context);
const args = gqlContext.getArgs();
const allowed = await relationAbilityChecker(
'PipelineProgress',
ability,
this.prismaService.client,
args,
);
if (!allowed) {
return false;
}
return ability.can(AbilityAction.Create, 'PipelineProgress'); return ability.can(AbilityAction.Create, 'PipelineProgress');
} }
} }
@ -48,11 +66,22 @@ export class UpdatePipelineProgressAbilityHandler implements IAbilityHandler {
const gqlContext = GqlExecutionContext.create(context); const gqlContext = GqlExecutionContext.create(context);
const args = gqlContext.getArgs<PipelineProgressArgs>(); const args = gqlContext.getArgs<PipelineProgressArgs>();
const pipelineProgress = const pipelineProgress =
await this.prismaService.pipelineProgress.findFirst({ await this.prismaService.client.pipelineProgress.findFirst({
where: args.where, where: args.where,
}); });
assert(pipelineProgress, '', NotFoundException); assert(pipelineProgress, '', NotFoundException);
const allowed = await relationAbilityChecker(
'PipelineProgress',
ability,
this.prismaService.client,
args,
);
if (!allowed) {
return false;
}
return ability.can( return ability.can(
AbilityAction.Update, AbilityAction.Update,
subject('PipelineProgress', pipelineProgress), subject('PipelineProgress', pipelineProgress),
@ -68,7 +97,7 @@ export class DeletePipelineProgressAbilityHandler implements IAbilityHandler {
const gqlContext = GqlExecutionContext.create(context); const gqlContext = GqlExecutionContext.create(context);
const args = gqlContext.getArgs<PipelineProgressArgs>(); const args = gqlContext.getArgs<PipelineProgressArgs>();
const pipelineProgress = const pipelineProgress =
await this.prismaService.pipelineProgress.findFirst({ await this.prismaService.client.pipelineProgress.findFirst({
where: args.where, where: args.where,
}); });
assert(pipelineProgress, '', NotFoundException); assert(pipelineProgress, '', NotFoundException);

View File

@ -13,10 +13,12 @@ import { PrismaService } from 'src/database/prisma.service';
import { AbilityAction } from 'src/ability/ability.action'; import { AbilityAction } from 'src/ability/ability.action';
import { AppAbility } from 'src/ability/ability.factory'; import { AppAbility } from 'src/ability/ability.factory';
import { PipelineStageWhereInput } from 'src/core/@generated/pipeline-stage/pipeline-stage-where.input'; import { PipelineStageWhereInput } from 'src/core/@generated/pipeline-stage/pipeline-stage-where.input';
import { relationAbilityChecker } from 'src/ability/ability.util';
import { assert } from 'src/utils/assert'; import { assert } from 'src/utils/assert';
class PipelineStageArgs { class PipelineStageArgs {
where?: PipelineStageWhereInput; where?: PipelineStageWhereInput;
[key: string]: any;
} }
@Injectable() @Injectable()
@ -35,7 +37,23 @@ export class ReadPipelineStageAbilityHandler implements IAbilityHandler {
@Injectable() @Injectable()
export class CreatePipelineStageAbilityHandler implements IAbilityHandler { export class CreatePipelineStageAbilityHandler implements IAbilityHandler {
handle(ability: AppAbility) { constructor(private readonly prismaService: PrismaService) {}
async handle(ability: AppAbility, context: ExecutionContext) {
const gqlContext = GqlExecutionContext.create(context);
const args = gqlContext.getArgs();
const allowed = await relationAbilityChecker(
'PipelineStage',
ability,
this.prismaService.client,
args,
);
if (!allowed) {
return false;
}
return ability.can(AbilityAction.Create, 'PipelineStage'); return ability.can(AbilityAction.Create, 'PipelineStage');
} }
} }
@ -47,11 +65,23 @@ export class UpdatePipelineStageAbilityHandler implements IAbilityHandler {
async handle(ability: AppAbility, context: ExecutionContext) { async handle(ability: AppAbility, context: ExecutionContext) {
const gqlContext = GqlExecutionContext.create(context); const gqlContext = GqlExecutionContext.create(context);
const args = gqlContext.getArgs<PipelineStageArgs>(); const args = gqlContext.getArgs<PipelineStageArgs>();
const pipelineStage = await this.prismaService.pipelineStage.findFirst({ const pipelineStage =
where: args.where, await this.prismaService.client.pipelineStage.findFirst({
}); where: args.where,
});
assert(pipelineStage, '', NotFoundException); assert(pipelineStage, '', NotFoundException);
const allowed = await relationAbilityChecker(
'PipelineStage',
ability,
this.prismaService.client,
args,
);
if (!allowed) {
return false;
}
return ability.can( return ability.can(
AbilityAction.Update, AbilityAction.Update,
subject('PipelineStage', pipelineStage), subject('PipelineStage', pipelineStage),
@ -66,9 +96,10 @@ export class DeletePipelineStageAbilityHandler implements IAbilityHandler {
async handle(ability: AppAbility, context: ExecutionContext) { async handle(ability: AppAbility, context: ExecutionContext) {
const gqlContext = GqlExecutionContext.create(context); const gqlContext = GqlExecutionContext.create(context);
const args = gqlContext.getArgs<PipelineStageArgs>(); const args = gqlContext.getArgs<PipelineStageArgs>();
const pipelineStage = await this.prismaService.pipelineStage.findFirst({ const pipelineStage =
where: args.where, await this.prismaService.client.pipelineStage.findFirst({
}); where: args.where,
});
assert(pipelineStage, '', NotFoundException); assert(pipelineStage, '', NotFoundException);
return ability.can( return ability.can(

View File

@ -13,10 +13,12 @@ import { PrismaService } from 'src/database/prisma.service';
import { AbilityAction } from 'src/ability/ability.action'; import { AbilityAction } from 'src/ability/ability.action';
import { AppAbility } from 'src/ability/ability.factory'; import { AppAbility } from 'src/ability/ability.factory';
import { PipelineWhereInput } from 'src/core/@generated/pipeline/pipeline-where.input'; import { PipelineWhereInput } from 'src/core/@generated/pipeline/pipeline-where.input';
import { relationAbilityChecker } from 'src/ability/ability.util';
import { assert } from 'src/utils/assert'; import { assert } from 'src/utils/assert';
class PipelineArgs { class PipelineArgs {
where?: PipelineWhereInput; where?: PipelineWhereInput;
[key: string]: any;
} }
@Injectable() @Injectable()
@ -35,7 +37,23 @@ export class ReadPipelineAbilityHandler implements IAbilityHandler {
@Injectable() @Injectable()
export class CreatePipelineAbilityHandler implements IAbilityHandler { export class CreatePipelineAbilityHandler implements IAbilityHandler {
handle(ability: AppAbility) { constructor(private readonly prismaService: PrismaService) {}
async handle(ability: AppAbility, context: ExecutionContext) {
const gqlContext = GqlExecutionContext.create(context);
const args = gqlContext.getArgs();
const allowed = await relationAbilityChecker(
'Pipeline',
ability,
this.prismaService.client,
args,
);
if (!allowed) {
return false;
}
return ability.can(AbilityAction.Create, 'Pipeline'); return ability.can(AbilityAction.Create, 'Pipeline');
} }
} }
@ -47,11 +65,22 @@ export class UpdatePipelineAbilityHandler implements IAbilityHandler {
async handle(ability: AppAbility, context: ExecutionContext) { async handle(ability: AppAbility, context: ExecutionContext) {
const gqlContext = GqlExecutionContext.create(context); const gqlContext = GqlExecutionContext.create(context);
const args = gqlContext.getArgs<PipelineArgs>(); const args = gqlContext.getArgs<PipelineArgs>();
const pipeline = await this.prismaService.pipeline.findFirst({ const pipeline = await this.prismaService.client.pipeline.findFirst({
where: args.where, where: args.where,
}); });
assert(pipeline, '', NotFoundException); assert(pipeline, '', NotFoundException);
const allowed = await relationAbilityChecker(
'Pipeline',
ability,
this.prismaService.client,
args,
);
if (!allowed) {
return false;
}
return ability.can(AbilityAction.Update, subject('Pipeline', pipeline)); return ability.can(AbilityAction.Update, subject('Pipeline', pipeline));
} }
} }
@ -63,7 +92,7 @@ export class DeletePipelineAbilityHandler implements IAbilityHandler {
async handle(ability: AppAbility, context: ExecutionContext) { async handle(ability: AppAbility, context: ExecutionContext) {
const gqlContext = GqlExecutionContext.create(context); const gqlContext = GqlExecutionContext.create(context);
const args = gqlContext.getArgs<PipelineArgs>(); const args = gqlContext.getArgs<PipelineArgs>();
const pipeline = await this.prismaService.pipeline.findFirst({ const pipeline = await this.prismaService.client.pipeline.findFirst({
where: args.where, where: args.where,
}); });
assert(pipeline, '', NotFoundException); assert(pipeline, '', NotFoundException);

View File

@ -13,10 +13,12 @@ import { PrismaService } from 'src/database/prisma.service';
import { AbilityAction } from 'src/ability/ability.action'; import { AbilityAction } from 'src/ability/ability.action';
import { AppAbility } from 'src/ability/ability.factory'; import { AppAbility } from 'src/ability/ability.factory';
import { RefreshTokenWhereInput } from 'src/core/@generated/refresh-token/refresh-token-where.input'; import { RefreshTokenWhereInput } from 'src/core/@generated/refresh-token/refresh-token-where.input';
import { relationAbilityChecker } from 'src/ability/ability.util';
import { assert } from 'src/utils/assert'; import { assert } from 'src/utils/assert';
class RefreshTokenArgs { class RefreshTokenArgs {
where?: RefreshTokenWhereInput; where?: RefreshTokenWhereInput;
[key: string]: any;
} }
@Injectable() @Injectable()
@ -35,7 +37,23 @@ export class ReadRefreshTokenAbilityHandler implements IAbilityHandler {
@Injectable() @Injectable()
export class CreateRefreshTokenAbilityHandler implements IAbilityHandler { export class CreateRefreshTokenAbilityHandler implements IAbilityHandler {
handle(ability: AppAbility) { constructor(private readonly prismaService: PrismaService) {}
async handle(ability: AppAbility, context: ExecutionContext) {
const gqlContext = GqlExecutionContext.create(context);
const args = gqlContext.getArgs();
const allowed = await relationAbilityChecker(
'RefreshToken',
ability,
this.prismaService.client,
args,
);
if (!allowed) {
return false;
}
return ability.can(AbilityAction.Create, 'RefreshToken'); return ability.can(AbilityAction.Create, 'RefreshToken');
} }
} }
@ -47,11 +65,24 @@ export class UpdateRefreshTokenAbilityHandler implements IAbilityHandler {
async handle(ability: AppAbility, context: ExecutionContext) { async handle(ability: AppAbility, context: ExecutionContext) {
const gqlContext = GqlExecutionContext.create(context); const gqlContext = GqlExecutionContext.create(context);
const args = gqlContext.getArgs<RefreshTokenArgs>(); const args = gqlContext.getArgs<RefreshTokenArgs>();
const refreshToken = await this.prismaService.refreshToken.findFirst({ const refreshToken = await this.prismaService.client.refreshToken.findFirst(
where: args.where, {
}); where: args.where,
},
);
assert(refreshToken, '', NotFoundException); assert(refreshToken, '', NotFoundException);
const allowed = await relationAbilityChecker(
'RefreshToken',
ability,
this.prismaService.client,
args,
);
if (!allowed) {
return false;
}
return ability.can( return ability.can(
AbilityAction.Update, AbilityAction.Update,
subject('RefreshToken', refreshToken), subject('RefreshToken', refreshToken),
@ -66,9 +97,11 @@ export class DeleteRefreshTokenAbilityHandler implements IAbilityHandler {
async handle(ability: AppAbility, context: ExecutionContext) { async handle(ability: AppAbility, context: ExecutionContext) {
const gqlContext = GqlExecutionContext.create(context); const gqlContext = GqlExecutionContext.create(context);
const args = gqlContext.getArgs<RefreshTokenArgs>(); const args = gqlContext.getArgs<RefreshTokenArgs>();
const refreshToken = await this.prismaService.refreshToken.findFirst({ const refreshToken = await this.prismaService.client.refreshToken.findFirst(
where: args.where, {
}); where: args.where,
},
);
assert(refreshToken, '', NotFoundException); assert(refreshToken, '', NotFoundException);
return ability.can( return ability.can(

View File

@ -12,11 +12,13 @@ import { IAbilityHandler } from 'src/ability/interfaces/ability-handler.interfac
import { PrismaService } from 'src/database/prisma.service'; import { PrismaService } from 'src/database/prisma.service';
import { AbilityAction } from 'src/ability/ability.action'; import { AbilityAction } from 'src/ability/ability.action';
import { AppAbility } from 'src/ability/ability.factory'; import { AppAbility } from 'src/ability/ability.factory';
import { assert } from 'src/utils/assert';
import { UserWhereInput } from 'src/core/@generated/user/user-where.input'; import { UserWhereInput } from 'src/core/@generated/user/user-where.input';
import { relationAbilityChecker } from 'src/ability/ability.util';
import { assert } from 'src/utils/assert';
class UserArgs { class UserArgs {
where?: UserWhereInput; where?: UserWhereInput;
[key: string]: any;
} }
@Injectable() @Injectable()
@ -35,7 +37,23 @@ export class ReadUserAbilityHandler implements IAbilityHandler {
@Injectable() @Injectable()
export class CreateUserAbilityHandler implements IAbilityHandler { export class CreateUserAbilityHandler implements IAbilityHandler {
handle(ability: AppAbility) { constructor(private readonly prismaService: PrismaService) {}
async handle(ability: AppAbility, context: ExecutionContext) {
const gqlContext = GqlExecutionContext.create(context);
const args = gqlContext.getArgs();
const allowed = await relationAbilityChecker(
'User',
ability,
this.prismaService.client,
args,
);
if (!allowed) {
return false;
}
return ability.can(AbilityAction.Create, 'User'); return ability.can(AbilityAction.Create, 'User');
} }
} }
@ -47,11 +65,22 @@ export class UpdateUserAbilityHandler implements IAbilityHandler {
async handle(ability: AppAbility, context: ExecutionContext) { async handle(ability: AppAbility, context: ExecutionContext) {
const gqlContext = GqlExecutionContext.create(context); const gqlContext = GqlExecutionContext.create(context);
const args = gqlContext.getArgs<UserArgs>(); const args = gqlContext.getArgs<UserArgs>();
const user = await this.prismaService.user.findFirst({ const user = await this.prismaService.client.user.findFirst({
where: args.where, where: args.where,
}); });
assert(user, '', NotFoundException); assert(user, '', NotFoundException);
const allowed = await relationAbilityChecker(
'User',
ability,
this.prismaService.client,
args,
);
if (!allowed) {
return false;
}
return ability.can(AbilityAction.Update, subject('User', user)); return ability.can(AbilityAction.Update, subject('User', user));
} }
} }
@ -63,7 +92,7 @@ export class DeleteUserAbilityHandler implements IAbilityHandler {
async handle(ability: AppAbility, context: ExecutionContext) { async handle(ability: AppAbility, context: ExecutionContext) {
const gqlContext = GqlExecutionContext.create(context); const gqlContext = GqlExecutionContext.create(context);
const args = gqlContext.getArgs<UserArgs>(); const args = gqlContext.getArgs<UserArgs>();
const user = await this.prismaService.user.findFirst({ const user = await this.prismaService.client.user.findFirst({
where: args.where, where: args.where,
}); });
assert(user, '', NotFoundException); assert(user, '', NotFoundException);

View File

@ -13,10 +13,12 @@ import { PrismaService } from 'src/database/prisma.service';
import { AbilityAction } from 'src/ability/ability.action'; import { AbilityAction } from 'src/ability/ability.action';
import { AppAbility } from 'src/ability/ability.factory'; import { AppAbility } from 'src/ability/ability.factory';
import { WorkspaceMemberWhereInput } from 'src/core/@generated/workspace-member/workspace-member-where.input'; import { WorkspaceMemberWhereInput } from 'src/core/@generated/workspace-member/workspace-member-where.input';
import { relationAbilityChecker } from 'src/ability/ability.util';
import { assert } from 'src/utils/assert'; import { assert } from 'src/utils/assert';
class WorksapceMemberArgs { class WorkspaceMemberArgs {
where?: WorkspaceMemberWhereInput; where?: WorkspaceMemberWhereInput;
[key: string]: any;
} }
@Injectable() @Injectable()
@ -35,7 +37,23 @@ export class ReadWorkspaceMemberAbilityHandler implements IAbilityHandler {
@Injectable() @Injectable()
export class CreateWorkspaceMemberAbilityHandler implements IAbilityHandler { export class CreateWorkspaceMemberAbilityHandler implements IAbilityHandler {
handle(ability: AppAbility) { constructor(private readonly prismaService: PrismaService) {}
async handle(ability: AppAbility, context: ExecutionContext) {
const gqlContext = GqlExecutionContext.create(context);
const args = gqlContext.getArgs();
const allowed = await relationAbilityChecker(
'WorkspaceMember',
ability,
this.prismaService.client,
args,
);
if (!allowed) {
return false;
}
return ability.can(AbilityAction.Create, 'WorkspaceMember'); return ability.can(AbilityAction.Create, 'WorkspaceMember');
} }
} }
@ -46,12 +64,24 @@ export class UpdateWorkspaceMemberAbilityHandler implements IAbilityHandler {
async handle(ability: AppAbility, context: ExecutionContext) { async handle(ability: AppAbility, context: ExecutionContext) {
const gqlContext = GqlExecutionContext.create(context); const gqlContext = GqlExecutionContext.create(context);
const args = gqlContext.getArgs<WorksapceMemberArgs>(); const args = gqlContext.getArgs<WorkspaceMemberArgs>();
const workspaceMember = await this.prismaService.workspaceMember.findFirst({ const workspaceMember =
where: args.where, await this.prismaService.client.workspaceMember.findFirst({
}); where: args.where,
});
assert(workspaceMember, '', NotFoundException); assert(workspaceMember, '', NotFoundException);
const allowed = await relationAbilityChecker(
'WorkspaceMember',
ability,
this.prismaService.client,
args,
);
if (!allowed) {
return false;
}
return ability.can( return ability.can(
AbilityAction.Update, AbilityAction.Update,
subject('WorkspaceMember', workspaceMember), subject('WorkspaceMember', workspaceMember),
@ -65,10 +95,11 @@ export class DeleteWorkspaceMemberAbilityHandler implements IAbilityHandler {
async handle(ability: AppAbility, context: ExecutionContext) { async handle(ability: AppAbility, context: ExecutionContext) {
const gqlContext = GqlExecutionContext.create(context); const gqlContext = GqlExecutionContext.create(context);
const args = gqlContext.getArgs<WorksapceMemberArgs>(); const args = gqlContext.getArgs<WorkspaceMemberArgs>();
const workspaceMember = await this.prismaService.workspaceMember.findFirst({ const workspaceMember =
where: args.where, await this.prismaService.client.workspaceMember.findFirst({
}); where: args.where,
});
assert(workspaceMember, '', NotFoundException); assert(workspaceMember, '', NotFoundException);
return ability.can( return ability.can(

View File

@ -1,24 +1,22 @@
import { import {
ExecutionContext, ExecutionContext,
ForbiddenException,
Injectable, Injectable,
NotFoundException, NotFoundException,
} from '@nestjs/common'; } from '@nestjs/common';
import { GqlExecutionContext } from '@nestjs/graphql'; import { GqlExecutionContext } from '@nestjs/graphql';
import { subject } from '@casl/ability';
import { IAbilityHandler } from 'src/ability/interfaces/ability-handler.interface'; import { IAbilityHandler } from 'src/ability/interfaces/ability-handler.interface';
import { PrismaService } from 'src/database/prisma.service'; import { PrismaService } from 'src/database/prisma.service';
import { AbilityAction } from 'src/ability/ability.action'; import { AbilityAction } from 'src/ability/ability.action';
import { AppAbility } from 'src/ability/ability.factory'; import { AppAbility } from 'src/ability/ability.factory';
import { WorkspaceWhereInput } from 'src/core/@generated/workspace/workspace-where.input'; import { WorkspaceWhereInput } from 'src/core/@generated/workspace/workspace-where.input';
import { relationAbilityChecker } from 'src/ability/ability.util';
import { assert } from 'src/utils/assert'; import { assert } from 'src/utils/assert';
import { getRequest } from 'src/utils/extract-request';
class WorksapceArgs { class WorkspaceArgs {
where?: WorkspaceWhereInput; where?: WorkspaceWhereInput;
[key: string]: any;
} }
@Injectable() @Injectable()
@ -37,7 +35,23 @@ export class ReadWorkspaceAbilityHandler implements IAbilityHandler {
@Injectable() @Injectable()
export class CreateWorkspaceAbilityHandler implements IAbilityHandler { export class CreateWorkspaceAbilityHandler implements IAbilityHandler {
handle(ability: AppAbility) { constructor(private readonly prismaService: PrismaService) {}
async handle(ability: AppAbility, context: ExecutionContext) {
const gqlContext = GqlExecutionContext.create(context);
const args = gqlContext.getArgs();
const allowed = await relationAbilityChecker(
'Workspace',
ability,
this.prismaService.client,
args,
);
if (!allowed) {
return false;
}
return ability.can(AbilityAction.Create, 'Workspace'); return ability.can(AbilityAction.Create, 'Workspace');
} }
} }
@ -47,15 +61,25 @@ export class UpdateWorkspaceAbilityHandler implements IAbilityHandler {
constructor(private readonly prismaService: PrismaService) {} constructor(private readonly prismaService: PrismaService) {}
async handle(ability: AppAbility, context: ExecutionContext) { async handle(ability: AppAbility, context: ExecutionContext) {
const request = getRequest(context); const gqlContext = GqlExecutionContext.create(context);
assert(request.user.workspace.id, '', ForbiddenException); const args = gqlContext.getArgs<WorkspaceArgs>();
const workspace = await this.prismaService.client.workspace.findFirst({
const workspace = await this.prismaService.workspace.findUnique({ where: args.where,
where: { id: request.user.workspace.id },
}); });
assert(workspace, '', NotFoundException); assert(workspace, '', NotFoundException);
return ability.can(AbilityAction.Update, subject('Workspace', workspace)); const allowed = await relationAbilityChecker(
'Workspace',
ability,
this.prismaService.client,
args,
);
if (!allowed) {
return false;
}
return ability.can(AbilityAction.Update, 'Workspace');
} }
} }
@ -65,12 +89,12 @@ export class DeleteWorkspaceAbilityHandler implements IAbilityHandler {
async handle(ability: AppAbility, context: ExecutionContext) { async handle(ability: AppAbility, context: ExecutionContext) {
const gqlContext = GqlExecutionContext.create(context); const gqlContext = GqlExecutionContext.create(context);
const args = gqlContext.getArgs<WorksapceArgs>(); const args = gqlContext.getArgs<WorkspaceArgs>();
const workspace = await this.prismaService.workspace.findFirst({ const workspace = await this.prismaService.client.workspace.findFirst({
where: args.where, where: args.where,
}); });
assert(workspace, '', NotFoundException); assert(workspace, '', NotFoundException);
return ability.can(AbilityAction.Delete, subject('Workspace', workspace)); return ability.can(AbilityAction.Delete, 'Workspace');
} }
} }

View File

@ -9,35 +9,35 @@ export class AttachmentService {
constructor(private readonly prismaService: PrismaService) {} constructor(private readonly prismaService: PrismaService) {}
// Find // Find
findFirst = this.prismaService.attachment.findFirst; findFirst = this.prismaService.client.attachment.findFirst;
findFirstOrThrow = this.prismaService.attachment.findFirstOrThrow; findFirstOrThrow = this.prismaService.client.attachment.findFirstOrThrow;
findUnique = this.prismaService.attachment.findUnique; findUnique = this.prismaService.client.attachment.findUnique;
findUniqueOrThrow = this.prismaService.attachment.findUniqueOrThrow; findUniqueOrThrow = this.prismaService.client.attachment.findUniqueOrThrow;
findMany = this.prismaService.attachment.findMany; findMany = this.prismaService.client.attachment.findMany;
// Create // Create
create = this.prismaService.attachment.create; create = this.prismaService.client.attachment.create;
createMany = this.prismaService.attachment.createMany; createMany = this.prismaService.client.attachment.createMany;
// Update // Update
update = this.prismaService.attachment.update; update = this.prismaService.client.attachment.update;
upsert = this.prismaService.attachment.upsert; upsert = this.prismaService.client.attachment.upsert;
updateMany = this.prismaService.attachment.updateMany; updateMany = this.prismaService.client.attachment.updateMany;
// Delete // Delete
delete = this.prismaService.attachment.delete; delete = this.prismaService.client.attachment.delete;
deleteMany = this.prismaService.attachment.deleteMany; deleteMany = this.prismaService.client.attachment.deleteMany;
// Aggregate // Aggregate
aggregate = this.prismaService.attachment.aggregate; aggregate = this.prismaService.client.attachment.aggregate;
// Count // Count
count = this.prismaService.attachment.count; count = this.prismaService.client.attachment.count;
// GroupBy // GroupBy
groupBy = this.prismaService.attachment.groupBy; groupBy = this.prismaService.client.attachment.groupBy;
getFileTypeFromFileName(fileName: string): AttachmentType { getFileTypeFromFileName(fileName: string): AttachmentType {
const extension = fileName.split('.').pop()?.toLowerCase(); const extension = fileName.split('.').pop()?.toLowerCase();

View File

@ -31,7 +31,7 @@ export class TokenService {
assert(expiresIn, '', InternalServerErrorException); assert(expiresIn, '', InternalServerErrorException);
const expiresAt = addMilliseconds(new Date().getTime(), ms(expiresIn)); const expiresAt = addMilliseconds(new Date().getTime(), ms(expiresIn));
const user = await this.prismaService.user.findUnique({ const user = await this.prismaService.client.user.findUnique({
where: { id: userId }, where: { id: userId },
include: { include: {
workspaceMember: true, workspaceMember: true,
@ -71,7 +71,7 @@ export class TokenService {
sub: userId, sub: userId,
}; };
const refreshToken = await this.prismaService.refreshToken.create({ const refreshToken = await this.prismaService.client.refreshToken.create({
data: refreshTokenPayload, data: refreshTokenPayload,
}); });
@ -122,13 +122,13 @@ export class TokenService {
UnprocessableEntityException, UnprocessableEntityException,
); );
const token = await this.prismaService.refreshToken.findUnique({ const token = await this.prismaService.client.refreshToken.findUnique({
where: { id: jwtPayload.jti }, where: { id: jwtPayload.jti },
}); });
assert(token, "This refresh token doesn't exist", NotFoundException); assert(token, "This refresh token doesn't exist", NotFoundException);
const user = await this.prismaService.user.findUnique({ const user = await this.prismaService.client.user.findUnique({
where: { where: {
id: jwtPayload.sub, id: jwtPayload.sub,
}, },
@ -141,7 +141,7 @@ export class TokenService {
if (token.isRevoked) { if (token.isRevoked) {
// Revoke all user refresh tokens // Revoke all user refresh tokens
await this.prismaService.refreshToken.updateMany({ await this.prismaService.client.refreshToken.updateMany({
where: { where: {
id: { id: {
in: user.refreshTokens.map(({ id }) => id), in: user.refreshTokens.map(({ id }) => id),
@ -172,7 +172,7 @@ export class TokenService {
} = await this.verifyRefreshToken(token); } = await this.verifyRefreshToken(token);
// Revoke old refresh token // Revoke old refresh token
await this.prismaService.refreshToken.update({ await this.prismaService.client.refreshToken.update({
where: { where: {
id, id,
}, },

View File

@ -24,7 +24,7 @@ export class JwtAuthStrategy extends PassportStrategy(Strategy, 'jwt') {
} }
async validate(payload: JwtPayload): Promise<PassportUser> { async validate(payload: JwtPayload): Promise<PassportUser> {
const user = await this.prismaService.user.findUniqueOrThrow({ const user = await this.prismaService.client.user.findUniqueOrThrow({
where: { id: payload.sub }, where: { id: payload.sub },
}); });
@ -32,9 +32,10 @@ export class JwtAuthStrategy extends PassportStrategy(Strategy, 'jwt') {
throw new UnauthorizedException(); throw new UnauthorizedException();
} }
const workspace = await this.prismaService.workspace.findUniqueOrThrow({ const workspace =
where: { id: payload.workspaceId }, await this.prismaService.client.workspace.findUniqueOrThrow({
}); where: { id: payload.workspaceId },
});
if (!workspace) { if (!workspace) {
throw new UnauthorizedException(); throw new UnauthorizedException();

View File

@ -1,9 +1,6 @@
import { Test, TestingModule } from '@nestjs/testing'; import { Test, TestingModule } from '@nestjs/testing';
import { CanActivate } from '@nestjs/common';
import { CommentThreadService } from 'src/core/comment/services/comment-thread.service'; import { CommentThreadService } from 'src/core/comment/services/comment-thread.service';
import { CreateOneCommentGuard } from 'src/guards/create-one-comment.guard';
import { CreateOneCommentThreadGuard } from 'src/guards/create-one-comment-thread.guard';
import { AbilityFactory } from 'src/ability/ability.factory'; import { AbilityFactory } from 'src/ability/ability.factory';
import { CommentThreadResolver } from './comment-thread.resolver'; import { CommentThreadResolver } from './comment-thread.resolver';
@ -12,8 +9,6 @@ describe('CommentThreadResolver', () => {
let resolver: CommentThreadResolver; let resolver: CommentThreadResolver;
beforeEach(async () => { beforeEach(async () => {
const mockGuard: CanActivate = { canActivate: jest.fn(() => true) };
const module: TestingModule = await Test.createTestingModule({ const module: TestingModule = await Test.createTestingModule({
providers: [ providers: [
CommentThreadResolver, CommentThreadResolver,
@ -26,12 +21,7 @@ describe('CommentThreadResolver', () => {
useValue: {}, useValue: {},
}, },
], ],
}) }).compile();
.overrideGuard(CreateOneCommentGuard)
.useValue(mockGuard)
.overrideGuard(CreateOneCommentThreadGuard)
.useValue(mockGuard)
.compile();
resolver = module.get<CommentThreadResolver>(CommentThreadResolver); resolver = module.get<CommentThreadResolver>(CommentThreadResolver);
}); });

View File

@ -9,7 +9,6 @@ import { Workspace } from 'src/core/@generated/workspace/workspace.model';
import { AuthWorkspace } from 'src/decorators/auth-workspace.decorator'; import { AuthWorkspace } from 'src/decorators/auth-workspace.decorator';
import { CommentThread } from 'src/core/@generated/comment-thread/comment-thread.model'; import { CommentThread } from 'src/core/@generated/comment-thread/comment-thread.model';
import { CreateOneCommentThreadArgs } from 'src/core/@generated/comment-thread/create-one-comment-thread.args'; import { CreateOneCommentThreadArgs } from 'src/core/@generated/comment-thread/create-one-comment-thread.args';
import { CreateOneCommentThreadGuard } from 'src/guards/create-one-comment-thread.guard';
import { FindManyCommentThreadArgs } from 'src/core/@generated/comment-thread/find-many-comment-thread.args'; import { FindManyCommentThreadArgs } from 'src/core/@generated/comment-thread/find-many-comment-thread.args';
import { CommentThreadService } from 'src/core/comment/services/comment-thread.service'; import { CommentThreadService } from 'src/core/comment/services/comment-thread.service';
import { UpdateOneCommentThreadArgs } from 'src/core/@generated/comment-thread/update-one-comment-thread.args'; import { UpdateOneCommentThreadArgs } from 'src/core/@generated/comment-thread/update-one-comment-thread.args';
@ -35,7 +34,6 @@ import { DeleteManyCommentThreadArgs } from 'src/core/@generated/comment-thread/
export class CommentThreadResolver { export class CommentThreadResolver {
constructor(private readonly commentThreadService: CommentThreadService) {} constructor(private readonly commentThreadService: CommentThreadService) {}
@UseGuards(CreateOneCommentThreadGuard)
@Mutation(() => CommentThread, { @Mutation(() => CommentThread, {
nullable: false, nullable: false,
}) })
@ -51,6 +49,15 @@ export class CommentThreadResolver {
data: { data: {
...args.data, ...args.data,
...{ workspace: { connect: { id: workspace.id } } }, ...{ workspace: { connect: { id: workspace.id } } },
commentThreadTargets: args.data?.commentThreadTargets?.createMany
? {
createMany: {
data: args.data.commentThreadTargets.createMany.data.map(
(target) => ({ ...target, workspaceId: workspace.id }),
),
},
}
: undefined,
}, },
select: prismaSelect.value, select: prismaSelect.value,
} as Prisma.CommentThreadCreateArgs); } as Prisma.CommentThreadCreateArgs);
@ -65,6 +72,7 @@ export class CommentThreadResolver {
@CheckAbilities(UpdateCommentThreadAbilityHandler) @CheckAbilities(UpdateCommentThreadAbilityHandler)
async updateOneCommentThread( async updateOneCommentThread(
@Args() args: UpdateOneCommentThreadArgs, @Args() args: UpdateOneCommentThreadArgs,
@AuthWorkspace() workspace: Workspace,
@PrismaSelector({ modelName: 'CommentThread' }) @PrismaSelector({ modelName: 'CommentThread' })
prismaSelect: PrismaSelect<'CommentThread'>, prismaSelect: PrismaSelect<'CommentThread'>,
): Promise<Partial<CommentThread>> { ): Promise<Partial<CommentThread>> {
@ -84,7 +92,18 @@ export class CommentThreadResolver {
} }
const updatedCommentThread = await this.commentThreadService.update({ const updatedCommentThread = await this.commentThreadService.update({
where: args.where, where: args.where,
data: args.data, data: {
...args.data,
commentThreadTargets: args.data?.commentThreadTargets?.createMany
? {
createMany: {
data: args.data.commentThreadTargets.createMany.data.map(
(target) => ({ ...target, workspaceId: workspace.id }),
),
},
}
: undefined,
},
select: prismaSelect.value, select: prismaSelect.value,
} as Prisma.CommentThreadUpdateArgs); } as Prisma.CommentThreadUpdateArgs);

View File

@ -1,8 +1,6 @@
import { Test, TestingModule } from '@nestjs/testing'; import { Test, TestingModule } from '@nestjs/testing';
import { CanActivate } from '@nestjs/common';
import { CommentService } from 'src/core/comment/services/comment.service'; import { CommentService } from 'src/core/comment/services/comment.service';
import { CreateOneCommentGuard } from 'src/guards/create-one-comment.guard';
import { AbilityFactory } from 'src/ability/ability.factory'; import { AbilityFactory } from 'src/ability/ability.factory';
import { CommentResolver } from './comment.resolver'; import { CommentResolver } from './comment.resolver';
@ -11,8 +9,6 @@ describe('CommentResolver', () => {
let resolver: CommentResolver; let resolver: CommentResolver;
beforeEach(async () => { beforeEach(async () => {
const mockGuard: CanActivate = { canActivate: jest.fn(() => true) };
const module: TestingModule = await Test.createTestingModule({ const module: TestingModule = await Test.createTestingModule({
providers: [ providers: [
CommentResolver, CommentResolver,
@ -25,10 +21,7 @@ describe('CommentResolver', () => {
useValue: {}, useValue: {},
}, },
], ],
}) }).compile();
.overrideGuard(CreateOneCommentGuard)
.useValue(mockGuard)
.compile();
resolver = module.get<CommentResolver>(CommentResolver); resolver = module.get<CommentResolver>(CommentResolver);
}); });

View File

@ -8,7 +8,6 @@ import { Workspace } from 'src/core/@generated/workspace/workspace.model';
import { AuthWorkspace } from 'src/decorators/auth-workspace.decorator'; import { AuthWorkspace } from 'src/decorators/auth-workspace.decorator';
import { CreateOneCommentArgs } from 'src/core/@generated/comment/create-one-comment.args'; import { CreateOneCommentArgs } from 'src/core/@generated/comment/create-one-comment.args';
import { Comment } from 'src/core/@generated/comment/comment.model'; import { Comment } from 'src/core/@generated/comment/comment.model';
import { CreateOneCommentGuard } from 'src/guards/create-one-comment.guard';
import { CommentService } from 'src/core/comment/services/comment.service'; import { CommentService } from 'src/core/comment/services/comment.service';
import { import {
PrismaSelector, PrismaSelector,
@ -25,7 +24,6 @@ import { User } from 'src/core/@generated/user/user.model';
export class CommentResolver { export class CommentResolver {
constructor(private readonly commentService: CommentService) {} constructor(private readonly commentService: CommentService) {}
@UseGuards(CreateOneCommentGuard)
@Mutation(() => Comment, { @Mutation(() => Comment, {
nullable: false, nullable: false,
}) })

View File

@ -7,33 +7,35 @@ export class CommentThreadTargetService {
constructor(private readonly prismaService: PrismaService) {} constructor(private readonly prismaService: PrismaService) {}
// Find // Find
findFirst = this.prismaService.commentThreadTarget.findFirst; findFirst = this.prismaService.client.commentThreadTarget.findFirst;
findFirstOrThrow = this.prismaService.commentThreadTarget.findFirstOrThrow; findFirstOrThrow =
this.prismaService.client.commentThreadTarget.findFirstOrThrow;
findUnique = this.prismaService.commentThreadTarget.findUnique; findUnique = this.prismaService.client.commentThreadTarget.findUnique;
findUniqueOrThrow = this.prismaService.commentThreadTarget.findUniqueOrThrow; findUniqueOrThrow =
this.prismaService.client.commentThreadTarget.findUniqueOrThrow;
findMany = this.prismaService.commentThreadTarget.findMany; findMany = this.prismaService.client.commentThreadTarget.findMany;
// Create // Create
create = this.prismaService.commentThreadTarget.create; create = this.prismaService.client.commentThreadTarget.create;
createMany = this.prismaService.commentThreadTarget.createMany; createMany = this.prismaService.client.commentThreadTarget.createMany;
// Update // Update
update = this.prismaService.commentThreadTarget.update; update = this.prismaService.client.commentThreadTarget.update;
upsert = this.prismaService.commentThreadTarget.upsert; upsert = this.prismaService.client.commentThreadTarget.upsert;
updateMany = this.prismaService.commentThreadTarget.updateMany; updateMany = this.prismaService.client.commentThreadTarget.updateMany;
// Delete // Delete
delete = this.prismaService.commentThreadTarget.delete; delete = this.prismaService.client.commentThreadTarget.delete;
deleteMany = this.prismaService.commentThreadTarget.deleteMany; deleteMany = this.prismaService.client.commentThreadTarget.deleteMany;
// Aggregate // Aggregate
aggregate = this.prismaService.commentThreadTarget.aggregate; aggregate = this.prismaService.client.commentThreadTarget.aggregate;
// Count // Count
count = this.prismaService.commentThreadTarget.count; count = this.prismaService.client.commentThreadTarget.count;
// GroupBy // GroupBy
groupBy = this.prismaService.commentThreadTarget.groupBy; groupBy = this.prismaService.client.commentThreadTarget.groupBy;
} }

View File

@ -7,33 +7,33 @@ export class CommentThreadService {
constructor(private readonly prismaService: PrismaService) {} constructor(private readonly prismaService: PrismaService) {}
// Find // Find
findFirst = this.prismaService.commentThread.findFirst; findFirst = this.prismaService.client.commentThread.findFirst;
findFirstOrThrow = this.prismaService.commentThread.findFirstOrThrow; findFirstOrThrow = this.prismaService.client.commentThread.findFirstOrThrow;
findUnique = this.prismaService.commentThread.findUnique; findUnique = this.prismaService.client.commentThread.findUnique;
findUniqueOrThrow = this.prismaService.commentThread.findUniqueOrThrow; findUniqueOrThrow = this.prismaService.client.commentThread.findUniqueOrThrow;
findMany = this.prismaService.commentThread.findMany; findMany = this.prismaService.client.commentThread.findMany;
// Create // Create
create = this.prismaService.commentThread.create; create = this.prismaService.client.commentThread.create;
createMany = this.prismaService.commentThread.createMany; createMany = this.prismaService.client.commentThread.createMany;
// Update // Update
update = this.prismaService.commentThread.update; update = this.prismaService.client.commentThread.update;
upsert = this.prismaService.commentThread.upsert; upsert = this.prismaService.client.commentThread.upsert;
updateMany = this.prismaService.commentThread.updateMany; updateMany = this.prismaService.client.commentThread.updateMany;
// Delete // Delete
delete = this.prismaService.commentThread.delete; delete = this.prismaService.client.commentThread.delete;
deleteMany = this.prismaService.commentThread.deleteMany; deleteMany = this.prismaService.client.commentThread.deleteMany;
// Aggregate // Aggregate
aggregate = this.prismaService.commentThread.aggregate; aggregate = this.prismaService.client.commentThread.aggregate;
// Count // Count
count = this.prismaService.commentThread.count; count = this.prismaService.client.commentThread.count;
// GroupBy // GroupBy
groupBy = this.prismaService.commentThread.groupBy; groupBy = this.prismaService.client.commentThread.groupBy;
} }

View File

@ -7,33 +7,33 @@ export class CommentService {
constructor(private readonly prismaService: PrismaService) {} constructor(private readonly prismaService: PrismaService) {}
// Find // Find
findFirst = this.prismaService.comment.findFirst; findFirst = this.prismaService.client.comment.findFirst;
findFirstOrThrow = this.prismaService.comment.findFirstOrThrow; findFirstOrThrow = this.prismaService.client.comment.findFirstOrThrow;
findUnique = this.prismaService.comment.findUnique; findUnique = this.prismaService.client.comment.findUnique;
findUniqueOrThrow = this.prismaService.comment.findUniqueOrThrow; findUniqueOrThrow = this.prismaService.client.comment.findUniqueOrThrow;
findMany = this.prismaService.comment.findMany; findMany = this.prismaService.client.comment.findMany;
// Create // Create
create = this.prismaService.comment.create; create = this.prismaService.client.comment.create;
createMany = this.prismaService.comment.createMany; createMany = this.prismaService.client.comment.createMany;
// Update // Update
update = this.prismaService.comment.update; update = this.prismaService.client.comment.update;
upsert = this.prismaService.comment.upsert; upsert = this.prismaService.client.comment.upsert;
updateMany = this.prismaService.comment.updateMany; updateMany = this.prismaService.client.comment.updateMany;
// Delete // Delete
delete = this.prismaService.comment.delete; delete = this.prismaService.client.comment.delete;
deleteMany = this.prismaService.comment.deleteMany; deleteMany = this.prismaService.client.comment.deleteMany;
// Aggregate // Aggregate
aggregate = this.prismaService.comment.aggregate; aggregate = this.prismaService.client.comment.aggregate;
// Count // Count
count = this.prismaService.comment.count; count = this.prismaService.client.comment.count;
// GroupBy // GroupBy
groupBy = this.prismaService.comment.groupBy; groupBy = this.prismaService.client.comment.groupBy;
} }

View File

@ -1,9 +1,5 @@
import { Test, TestingModule } from '@nestjs/testing'; import { Test, TestingModule } from '@nestjs/testing';
import { CanActivate } from '@nestjs/common';
import { UpdateOneGuard } from 'src/guards/update-one.guard';
import { DeleteManyGuard } from 'src/guards/delete-many.guard';
import { CreateOneGuard } from 'src/guards/create-one.guard';
import { AbilityFactory } from 'src/ability/ability.factory'; import { AbilityFactory } from 'src/ability/ability.factory';
import { CompanyService } from './company.service'; import { CompanyService } from './company.service';
@ -13,8 +9,6 @@ describe('CompanyResolver', () => {
let resolver: CompanyResolver; let resolver: CompanyResolver;
beforeEach(async () => { beforeEach(async () => {
const mockGuard: CanActivate = { canActivate: jest.fn(() => true) };
const module: TestingModule = await Test.createTestingModule({ const module: TestingModule = await Test.createTestingModule({
providers: [ providers: [
CompanyResolver, CompanyResolver,
@ -27,14 +21,7 @@ describe('CompanyResolver', () => {
useValue: {}, useValue: {},
}, },
], ],
}) }).compile();
.overrideGuard(UpdateOneGuard)
.useValue(mockGuard)
.overrideGuard(DeleteManyGuard)
.useValue(mockGuard)
.overrideGuard(CreateOneGuard)
.useValue(mockGuard)
.compile();
resolver = module.get<CompanyResolver>(CompanyResolver); resolver = module.get<CompanyResolver>(CompanyResolver);
}); });

View File

@ -12,9 +12,6 @@ import { UpdateOneCompanyArgs } from 'src/core/@generated/company/update-one-com
import { CreateOneCompanyArgs } from 'src/core/@generated/company/create-one-company.args'; import { CreateOneCompanyArgs } from 'src/core/@generated/company/create-one-company.args';
import { AffectedRows } from 'src/core/@generated/prisma/affected-rows.output'; import { AffectedRows } from 'src/core/@generated/prisma/affected-rows.output';
import { DeleteManyCompanyArgs } from 'src/core/@generated/company/delete-many-company.args'; import { DeleteManyCompanyArgs } from 'src/core/@generated/company/delete-many-company.args';
import { UpdateOneGuard } from 'src/guards/update-one.guard';
import { DeleteManyGuard } from 'src/guards/delete-many.guard';
import { CreateOneGuard } from 'src/guards/create-one.guard';
import { import {
PrismaSelect, PrismaSelect,
PrismaSelector, PrismaSelector,
@ -78,7 +75,6 @@ export class CompanyResolver {
}); });
} }
@UseGuards(UpdateOneGuard)
@Mutation(() => Company, { @Mutation(() => Company, {
nullable: true, nullable: true,
}) })
@ -96,7 +92,6 @@ export class CompanyResolver {
} as Prisma.CompanyUpdateArgs); } as Prisma.CompanyUpdateArgs);
} }
@UseGuards(DeleteManyGuard)
@Mutation(() => AffectedRows, { @Mutation(() => AffectedRows, {
nullable: false, nullable: false,
}) })
@ -110,7 +105,6 @@ export class CompanyResolver {
}); });
} }
@UseGuards(CreateOneGuard)
@Mutation(() => Company, { @Mutation(() => Company, {
nullable: false, nullable: false,
}) })

View File

@ -8,35 +8,35 @@ export class CompanyService {
constructor(private readonly prismaService: PrismaService) {} constructor(private readonly prismaService: PrismaService) {}
// Find // Find
findFirst = this.prismaService.company.findFirst; findFirst = this.prismaService.client.company.findFirst;
findFirstOrThrow = this.prismaService.company.findFirstOrThrow; findFirstOrThrow = this.prismaService.client.company.findFirstOrThrow;
findUnique = this.prismaService.company.findUnique; findUnique = this.prismaService.client.company.findUnique;
findUniqueOrThrow = this.prismaService.company.findUniqueOrThrow; findUniqueOrThrow = this.prismaService.client.company.findUniqueOrThrow;
findMany = this.prismaService.company.findMany; findMany = this.prismaService.client.company.findMany;
// Create // Create
create = this.prismaService.company.create; create = this.prismaService.client.company.create;
createMany = this.prismaService.company.createMany; createMany = this.prismaService.client.company.createMany;
// Update // Update
update = this.prismaService.company.update; update = this.prismaService.client.company.update;
upsert = this.prismaService.company.upsert; upsert = this.prismaService.client.company.upsert;
updateMany = this.prismaService.company.updateMany; updateMany = this.prismaService.client.company.updateMany;
// Delete // Delete
delete = this.prismaService.company.delete; delete = this.prismaService.client.company.delete;
deleteMany = this.prismaService.company.deleteMany; deleteMany = this.prismaService.client.company.deleteMany;
// Aggregate // Aggregate
aggregate = this.prismaService.company.aggregate; aggregate = this.prismaService.client.company.aggregate;
// Count // Count
count = this.prismaService.company.count; count = this.prismaService.client.company.count;
// GroupBy // GroupBy
groupBy = this.prismaService.company.groupBy; groupBy = this.prismaService.client.company.groupBy;
async createDefaultCompanies({ workspaceId }: { workspaceId: string }) { async createDefaultCompanies({ workspaceId }: { workspaceId: string }) {
const companies = companiesSeed.map((company) => ({ const companies = companiesSeed.map((company) => ({
...company, ...company,

View File

@ -1,9 +1,5 @@
import { Test, TestingModule } from '@nestjs/testing'; import { Test, TestingModule } from '@nestjs/testing';
import { CanActivate } from '@nestjs/common';
import { UpdateOneGuard } from 'src/guards/update-one.guard';
import { DeleteManyGuard } from 'src/guards/delete-many.guard';
import { CreateOneGuard } from 'src/guards/create-one.guard';
import { AbilityFactory } from 'src/ability/ability.factory'; import { AbilityFactory } from 'src/ability/ability.factory';
import { PersonService } from './person.service'; import { PersonService } from './person.service';
@ -13,8 +9,6 @@ describe('PersonResolver', () => {
let resolver: PersonResolver; let resolver: PersonResolver;
beforeEach(async () => { beforeEach(async () => {
const mockGuard: CanActivate = { canActivate: jest.fn(() => true) };
const module: TestingModule = await Test.createTestingModule({ const module: TestingModule = await Test.createTestingModule({
providers: [ providers: [
PersonResolver, PersonResolver,
@ -27,14 +21,7 @@ describe('PersonResolver', () => {
useValue: {}, useValue: {},
}, },
], ],
}) }).compile();
.overrideGuard(UpdateOneGuard)
.useValue(mockGuard)
.overrideGuard(DeleteManyGuard)
.useValue(mockGuard)
.overrideGuard(CreateOneGuard)
.useValue(mockGuard)
.compile();
resolver = module.get<PersonResolver>(PersonResolver); resolver = module.get<PersonResolver>(PersonResolver);
}); });

View File

@ -20,9 +20,6 @@ import { AffectedRows } from 'src/core/@generated/prisma/affected-rows.output';
import { DeleteManyPersonArgs } from 'src/core/@generated/person/delete-many-person.args'; import { DeleteManyPersonArgs } from 'src/core/@generated/person/delete-many-person.args';
import { Workspace } from 'src/core/@generated/workspace/workspace.model'; import { Workspace } from 'src/core/@generated/workspace/workspace.model';
import { AuthWorkspace } from 'src/decorators/auth-workspace.decorator'; import { AuthWorkspace } from 'src/decorators/auth-workspace.decorator';
import { UpdateOneGuard } from 'src/guards/update-one.guard';
import { DeleteManyGuard } from 'src/guards/delete-many.guard';
import { CreateOneGuard } from 'src/guards/create-one.guard';
import { import {
PrismaSelect, PrismaSelect,
PrismaSelector, PrismaSelector,
@ -95,7 +92,6 @@ export class PersonResolver {
return `${parent.firstName ?? ''} ${parent.lastName ?? ''}`; return `${parent.firstName ?? ''} ${parent.lastName ?? ''}`;
} }
@UseGuards(UpdateOneGuard)
@Mutation(() => Person, { @Mutation(() => Person, {
nullable: true, nullable: true,
}) })
@ -128,7 +124,6 @@ export class PersonResolver {
} as Prisma.PersonUpdateArgs); } as Prisma.PersonUpdateArgs);
} }
@UseGuards(DeleteManyGuard)
@Mutation(() => AffectedRows, { @Mutation(() => AffectedRows, {
nullable: false, nullable: false,
}) })
@ -142,7 +137,6 @@ export class PersonResolver {
}); });
} }
@UseGuards(CreateOneGuard)
@Mutation(() => Person, { @Mutation(() => Person, {
nullable: false, nullable: false,
}) })

View File

@ -10,35 +10,35 @@ export class PersonService {
constructor(private readonly prismaService: PrismaService) {} constructor(private readonly prismaService: PrismaService) {}
// Find // Find
findFirst = this.prismaService.person.findFirst; findFirst = this.prismaService.client.person.findFirst;
findFirstOrThrow = this.prismaService.person.findFirstOrThrow; findFirstOrThrow = this.prismaService.client.person.findFirstOrThrow;
findUnique = this.prismaService.person.findUnique; findUnique = this.prismaService.client.person.findUnique;
findUniqueOrThrow = this.prismaService.person.findUniqueOrThrow; findUniqueOrThrow = this.prismaService.client.person.findUniqueOrThrow;
findMany = this.prismaService.person.findMany; findMany = this.prismaService.client.person.findMany;
// Create // Create
create = this.prismaService.person.create; create = this.prismaService.client.person.create;
createMany = this.prismaService.person.createMany; createMany = this.prismaService.client.person.createMany;
// Update // Update
update = this.prismaService.person.update; update = this.prismaService.client.person.update;
upsert = this.prismaService.person.upsert; upsert = this.prismaService.client.person.upsert;
updateMany = this.prismaService.person.updateMany; updateMany = this.prismaService.client.person.updateMany;
// Delete // Delete
delete = this.prismaService.person.delete; delete = this.prismaService.client.person.delete;
deleteMany = this.prismaService.person.deleteMany; deleteMany = this.prismaService.client.person.deleteMany;
// Aggregate // Aggregate
aggregate = this.prismaService.person.aggregate; aggregate = this.prismaService.client.person.aggregate;
// Count // Count
count = this.prismaService.person.count; count = this.prismaService.client.person.count;
// GroupBy // GroupBy
groupBy = this.prismaService.person.groupBy; groupBy = this.prismaService.client.person.groupBy;
async createDefaultPeople({ async createDefaultPeople({
workspaceId, workspaceId,
companies, companies,

View File

@ -7,33 +7,35 @@ export class PipelineProgressService {
constructor(private readonly prismaService: PrismaService) {} constructor(private readonly prismaService: PrismaService) {}
// Find // Find
findFirst = this.prismaService.pipelineProgress.findFirst; findFirst = this.prismaService.client.pipelineProgress.findFirst;
findFirstOrThrow = this.prismaService.pipelineProgress.findFirstOrThrow; findFirstOrThrow =
this.prismaService.client.pipelineProgress.findFirstOrThrow;
findUnique = this.prismaService.pipelineProgress.findUnique; findUnique = this.prismaService.client.pipelineProgress.findUnique;
findUniqueOrThrow = this.prismaService.pipelineProgress.findUniqueOrThrow; findUniqueOrThrow =
this.prismaService.client.pipelineProgress.findUniqueOrThrow;
findMany = this.prismaService.pipelineProgress.findMany; findMany = this.prismaService.client.pipelineProgress.findMany;
// Create // Create
create = this.prismaService.pipelineProgress.create; create = this.prismaService.client.pipelineProgress.create;
createMany = this.prismaService.pipelineProgress.createMany; createMany = this.prismaService.client.pipelineProgress.createMany;
// Update // Update
update = this.prismaService.pipelineProgress.update; update = this.prismaService.client.pipelineProgress.update;
upsert = this.prismaService.pipelineProgress.upsert; upsert = this.prismaService.client.pipelineProgress.upsert;
updateMany = this.prismaService.pipelineProgress.updateMany; updateMany = this.prismaService.client.pipelineProgress.updateMany;
// Delete // Delete
delete = this.prismaService.pipelineProgress.delete; delete = this.prismaService.client.pipelineProgress.delete;
deleteMany = this.prismaService.pipelineProgress.deleteMany; deleteMany = this.prismaService.client.pipelineProgress.deleteMany;
// Aggregate // Aggregate
aggregate = this.prismaService.pipelineProgress.aggregate; aggregate = this.prismaService.client.pipelineProgress.aggregate;
// Count // Count
count = this.prismaService.pipelineProgress.count; count = this.prismaService.client.pipelineProgress.count;
// GroupBy // GroupBy
groupBy = this.prismaService.pipelineProgress.groupBy; groupBy = this.prismaService.client.pipelineProgress.groupBy;
} }

View File

@ -8,35 +8,35 @@ export class PipelineStageService {
constructor(private readonly prismaService: PrismaService) {} constructor(private readonly prismaService: PrismaService) {}
// Find // Find
findFirst = this.prismaService.pipelineStage.findFirst; findFirst = this.prismaService.client.pipelineStage.findFirst;
findFirstOrThrow = this.prismaService.pipelineStage.findFirstOrThrow; findFirstOrThrow = this.prismaService.client.pipelineStage.findFirstOrThrow;
findUnique = this.prismaService.pipelineStage.findUnique; findUnique = this.prismaService.client.pipelineStage.findUnique;
findUniqueOrThrow = this.prismaService.pipelineStage.findUniqueOrThrow; findUniqueOrThrow = this.prismaService.client.pipelineStage.findUniqueOrThrow;
findMany = this.prismaService.pipelineStage.findMany; findMany = this.prismaService.client.pipelineStage.findMany;
// Create // Create
create = this.prismaService.pipelineStage.create; create = this.prismaService.client.pipelineStage.create;
createMany = this.prismaService.pipelineStage.createMany; createMany = this.prismaService.client.pipelineStage.createMany;
// Update // Update
update = this.prismaService.pipelineStage.update; update = this.prismaService.client.pipelineStage.update;
upsert = this.prismaService.pipelineStage.upsert; upsert = this.prismaService.client.pipelineStage.upsert;
updateMany = this.prismaService.pipelineStage.updateMany; updateMany = this.prismaService.client.pipelineStage.updateMany;
// Delete // Delete
delete = this.prismaService.pipelineStage.delete; delete = this.prismaService.client.pipelineStage.delete;
deleteMany = this.prismaService.pipelineStage.deleteMany; deleteMany = this.prismaService.client.pipelineStage.deleteMany;
// Aggregate // Aggregate
aggregate = this.prismaService.pipelineStage.aggregate; aggregate = this.prismaService.client.pipelineStage.aggregate;
// Count // Count
count = this.prismaService.pipelineStage.count; count = this.prismaService.client.pipelineStage.count;
// GroupBy // GroupBy
groupBy = this.prismaService.pipelineStage.groupBy; groupBy = this.prismaService.client.pipelineStage.groupBy;
// Customs // Customs
async createDefaultPipelineStages({ async createDefaultPipelineStages({

View File

@ -10,35 +10,35 @@ export class PipelineService {
constructor(private readonly prismaService: PrismaService) {} constructor(private readonly prismaService: PrismaService) {}
// Find // Find
findFirst = this.prismaService.pipeline.findFirst; findFirst = this.prismaService.client.pipeline.findFirst;
findFirstOrThrow = this.prismaService.pipeline.findFirstOrThrow; findFirstOrThrow = this.prismaService.client.pipeline.findFirstOrThrow;
findUnique = this.prismaService.pipeline.findUnique; findUnique = this.prismaService.client.pipeline.findUnique;
findUniqueOrThrow = this.prismaService.pipeline.findUniqueOrThrow; findUniqueOrThrow = this.prismaService.client.pipeline.findUniqueOrThrow;
findMany = this.prismaService.pipeline.findMany; findMany = this.prismaService.client.pipeline.findMany;
// Create // Create
create = this.prismaService.pipeline.create; create = this.prismaService.client.pipeline.create;
createMany = this.prismaService.pipeline.createMany; createMany = this.prismaService.client.pipeline.createMany;
// Update // Update
update = this.prismaService.pipeline.update; update = this.prismaService.client.pipeline.update;
upsert = this.prismaService.pipeline.upsert; upsert = this.prismaService.client.pipeline.upsert;
updateMany = this.prismaService.pipeline.updateMany; updateMany = this.prismaService.client.pipeline.updateMany;
// Delete // Delete
delete = this.prismaService.pipeline.delete; delete = this.prismaService.client.pipeline.delete;
deleteMany = this.prismaService.pipeline.deleteMany; deleteMany = this.prismaService.client.pipeline.deleteMany;
// Aggregate // Aggregate
aggregate = this.prismaService.pipeline.aggregate; aggregate = this.prismaService.client.pipeline.aggregate;
// Count // Count
count = this.prismaService.pipeline.count; count = this.prismaService.client.pipeline.count;
// GroupBy // GroupBy
groupBy = this.prismaService.pipeline.groupBy; groupBy = this.prismaService.client.pipeline.groupBy;
// Customs // Customs
async createDefaultPipeline({ workspaceId }: { workspaceId: string }) { async createDefaultPipeline({ workspaceId }: { workspaceId: string }) {

View File

@ -19,35 +19,35 @@ export class UserService {
) {} ) {}
// Find // Find
findFirst = this.prismaService.user.findFirst; findFirst = this.prismaService.client.user.findFirst;
findFirstOrThrow = this.prismaService.user.findFirstOrThrow; findFirstOrThrow = this.prismaService.client.user.findFirstOrThrow;
findUnique = this.prismaService.user.findUnique; findUnique = this.prismaService.client.user.findUnique;
findUniqueOrThrow = this.prismaService.user.findUniqueOrThrow; findUniqueOrThrow = this.prismaService.client.user.findUniqueOrThrow;
findMany = this.prismaService.user.findMany; findMany = this.prismaService.client.user.findMany;
// Create // Create
create = this.prismaService.user.create; create = this.prismaService.client.user.create;
createMany = this.prismaService.user.createMany; createMany = this.prismaService.client.user.createMany;
// Update // Update
update = this.prismaService.user.update; update = this.prismaService.client.user.update;
upsert = this.prismaService.user.upsert; upsert = this.prismaService.client.user.upsert;
updateMany = this.prismaService.user.updateMany; updateMany = this.prismaService.client.user.updateMany;
// Delete // Delete
delete = this.prismaService.user.delete; delete = this.prismaService.client.user.delete;
deleteMany = this.prismaService.user.deleteMany; deleteMany = this.prismaService.client.user.deleteMany;
// Aggregate // Aggregate
aggregate = this.prismaService.user.aggregate; aggregate = this.prismaService.client.user.aggregate;
// Count // Count
count = this.prismaService.user.count; count = this.prismaService.client.user.count;
// GroupBy // GroupBy
groupBy = this.prismaService.user.groupBy; groupBy = this.prismaService.client.user.groupBy;
// Customs // Customs
async createUser<T extends Prisma.UserCreateArgs>( async createUser<T extends Prisma.UserCreateArgs>(
@ -68,7 +68,7 @@ export class UserService {
assert(workspace, 'workspace is missing', BadRequestException); assert(workspace, 'workspace is missing', BadRequestException);
// Create user // Create user
const user = await this.prismaService.user.upsert({ const user = await this.prismaService.client.user.upsert({
where: { where: {
email: args.data.email, email: args.data.email,
}, },

View File

@ -7,33 +7,34 @@ export class WorkspaceMemberService {
constructor(private readonly prismaService: PrismaService) {} constructor(private readonly prismaService: PrismaService) {}
// Find // Find
findFirst = this.prismaService.workspaceMember.findFirst; findFirst = this.prismaService.client.workspaceMember.findFirst;
findFirstOrThrow = this.prismaService.workspaceMember.findFirstOrThrow; findFirstOrThrow = this.prismaService.client.workspaceMember.findFirstOrThrow;
findUnique = this.prismaService.workspaceMember.findUnique; findUnique = this.prismaService.client.workspaceMember.findUnique;
findUniqueOrThrow = this.prismaService.workspaceMember.findUniqueOrThrow; findUniqueOrThrow =
this.prismaService.client.workspaceMember.findUniqueOrThrow;
findMany = this.prismaService.workspaceMember.findMany; findMany = this.prismaService.client.workspaceMember.findMany;
// Create // Create
create = this.prismaService.workspaceMember.create; create = this.prismaService.client.workspaceMember.create;
createMany = this.prismaService.workspaceMember.createMany; createMany = this.prismaService.client.workspaceMember.createMany;
// Update // Update
update = this.prismaService.workspaceMember.update; update = this.prismaService.client.workspaceMember.update;
upsert = this.prismaService.workspaceMember.upsert; upsert = this.prismaService.client.workspaceMember.upsert;
updateMany = this.prismaService.workspaceMember.updateMany; updateMany = this.prismaService.client.workspaceMember.updateMany;
// Delete // Delete
delete = this.prismaService.workspaceMember.delete; delete = this.prismaService.client.workspaceMember.delete;
deleteMany = this.prismaService.workspaceMember.deleteMany; deleteMany = this.prismaService.client.workspaceMember.deleteMany;
// Aggregate // Aggregate
aggregate = this.prismaService.workspaceMember.aggregate; aggregate = this.prismaService.client.workspaceMember.aggregate;
// Count // Count
count = this.prismaService.workspaceMember.count; count = this.prismaService.client.workspaceMember.count;
// GroupBy // GroupBy
groupBy = this.prismaService.workspaceMember.groupBy; groupBy = this.prismaService.client.workspaceMember.groupBy;
} }

View File

@ -19,35 +19,35 @@ export class WorkspaceService {
) {} ) {}
// Find // Find
findFirst = this.prismaService.workspace.findFirst; findFirst = this.prismaService.client.workspace.findFirst;
findFirstOrThrow = this.prismaService.workspace.findFirstOrThrow; findFirstOrThrow = this.prismaService.client.workspace.findFirstOrThrow;
findUnique = this.prismaService.workspace.findUnique; findUnique = this.prismaService.client.workspace.findUnique;
findUniqueOrThrow = this.prismaService.workspace.findUniqueOrThrow; findUniqueOrThrow = this.prismaService.client.workspace.findUniqueOrThrow;
findMany = this.prismaService.workspace.findMany; findMany = this.prismaService.client.workspace.findMany;
// Create // Create
create = this.prismaService.workspace.create; create = this.prismaService.client.workspace.create;
createMany = this.prismaService.workspace.createMany; createMany = this.prismaService.client.workspace.createMany;
// Update // Update
update = this.prismaService.workspace.update; update = this.prismaService.client.workspace.update;
upsert = this.prismaService.workspace.upsert; upsert = this.prismaService.client.workspace.upsert;
updateMany = this.prismaService.workspace.updateMany; updateMany = this.prismaService.client.workspace.updateMany;
// Delete // Delete
delete = this.prismaService.workspace.delete; delete = this.prismaService.client.workspace.delete;
deleteMany = this.prismaService.workspace.deleteMany; deleteMany = this.prismaService.client.workspace.deleteMany;
// Aggregate // Aggregate
aggregate = this.prismaService.workspace.aggregate; aggregate = this.prismaService.client.workspace.aggregate;
// Count // Count
count = this.prismaService.workspace.count; count = this.prismaService.client.workspace.count;
// GroupBy // GroupBy
groupBy = this.prismaService.workspace.groupBy; groupBy = this.prismaService.client.workspace.groupBy;
// Customs // Customs
async createDefaultWorkspace() { async createDefaultWorkspace() {

View File

@ -0,0 +1,14 @@
/*
Warnings:
- Added the required column `workspaceId` to the `comment_thread_targets` table without a default value. This is not possible if the table is not empty.
*/
-- AlterTable
ALTER TABLE "comment_thread_targets" ADD COLUMN "workspaceId" TEXT NOT NULL;
-- AddForeignKey
ALTER TABLE "comment_thread_targets" ADD CONSTRAINT "comment_thread_targets_workspaceId_fkey" FOREIGN KEY ("workspaceId") REFERENCES "workspaces"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "attachments" ADD CONSTRAINT "attachments_workspaceId_fkey" FOREIGN KEY ("workspaceId") REFERENCES "workspaces"("id") ON DELETE RESTRICT ON UPDATE CASCADE;

View File

@ -5,24 +5,33 @@ import {
OnModuleInit, OnModuleInit,
} from '@nestjs/common'; } from '@nestjs/common';
import { PrismaClient } from '@prisma/client'; import { Prisma, PrismaClient } from '@prisma/client';
import { createPrismaQueryEventHandler } from 'prisma-query-log'; import { createPrismaQueryEventHandler } from 'prisma-query-log';
import { EnvironmentService } from 'src/integrations/environment/environment.service'; import { EnvironmentService } from 'src/integrations/environment/environment.service';
// TODO: Check if this is still needed // Prepare Prisma extenstion ability
if (!global.prisma) { const createPrismaClient = (options: Prisma.PrismaClientOptions) => {
global.prisma = new PrismaClient(); const client = new PrismaClient(options);
}
export default global.prisma; return client;
};
type ExtendedPrismaClient = ReturnType<typeof createPrismaClient>;
@Injectable() @Injectable()
export class PrismaService extends PrismaClient implements OnModuleInit { export class PrismaService implements OnModuleInit {
private readonly logger = new Logger(PrismaService.name); private readonly logger = new Logger(PrismaService.name);
private prismaClient!: ExtendedPrismaClient;
public get client(): ExtendedPrismaClient {
return this.prismaClient;
}
constructor(private readonly environmentService: EnvironmentService) { constructor(private readonly environmentService: EnvironmentService) {
const debugMode = environmentService.isDebugMode(); const debugMode = environmentService.isDebugMode();
super({
this.prismaClient = createPrismaClient({
errorFormat: 'minimal', errorFormat: 'minimal',
log: debugMode log: debugMode
? [ ? [
@ -44,16 +53,16 @@ export class PrismaService extends PrismaClient implements OnModuleInit {
colorParameter: '\u001B[90m', colorParameter: '\u001B[90m',
}); });
this.$on('query' as any, logHandler); this.prismaClient.$on('query' as any, logHandler);
} }
} }
async onModuleInit() { async onModuleInit(): Promise<void> {
await this.$connect(); await this.prismaClient.$connect();
} }
async enableShutdownHooks(app: INestApplication) { async enableShutdownHooks(app: INestApplication) {
this.$on('beforeExit', async () => { this.prismaClient.$on('beforeExit', async () => {
await app.close(); await app.close();
}); });
} }

View File

@ -173,8 +173,10 @@ model Workspace {
/// @TypeGraphQL.omit(input: true, output: true) /// @TypeGraphQL.omit(input: true, output: true)
deletedAt DateTime? deletedAt DateTime?
createdAt DateTime @default(now()) createdAt DateTime @default(now())
updatedAt DateTime @updatedAt updatedAt DateTime @updatedAt
Attachment Attachment[]
CommentThreadTarget CommentThreadTarget[]
@@map("workspaces") @@map("workspaces")
} }
@ -379,9 +381,12 @@ model CommentThreadTarget {
/// @Validator.IsOptional() /// @Validator.IsOptional()
id String @id @default(uuid()) id String @id @default(uuid())
commentThread CommentThread @relation(fields: [commentThreadId], references: [id], onDelete: Cascade) commentThread CommentThread @relation(fields: [commentThreadId], references: [id], onDelete: Cascade)
commentThreadId String commentThreadId String
/// @TypeGraphQL.omit(input: true, output: false)
workspace Workspace @relation(fields: [workspaceId], references: [id])
/// @TypeGraphQL.omit(input: true, output: true)
workspaceId String
commentableType CommentableType commentableType CommentableType
commentableId String commentableId String
@ -515,6 +520,8 @@ model Attachment {
activityId String activityId String
activity CommentThread @relation(fields: [activityId], references: [id]) activity CommentThread @relation(fields: [activityId], references: [id])
/// @TypeGraphQL.omit(input: true, output: false)
workspace Workspace @relation(fields: [workspaceId], references: [id])
/// @TypeGraphQL.omit(input: true, output: true) /// @TypeGraphQL.omit(input: true, output: true)
workspaceId String workspaceId String

View File

@ -18,6 +18,7 @@ export const seedComments = async (prisma: PrismaClient) => {
update: {}, update: {},
create: { create: {
id: 'twenty-fe256b39-3ec3-4fe3-8997-b76aa0bfb600', id: 'twenty-fe256b39-3ec3-4fe3-8997-b76aa0bfb600',
workspaceId: 'twenty-7ed9d212-1c25-4d02-bf25-6aeccf7ea419',
commentableType: 'Company', commentableType: 'Company',
commentableId: 'twenty-fe256b39-3ec3-4fe3-8997-b76aa0bfa408', commentableId: 'twenty-fe256b39-3ec3-4fe3-8997-b76aa0bfa408',
commentThreadId: 'twenty-fe256b39-3ec3-4fe3-8997-b76aa0bfb400', commentThreadId: 'twenty-fe256b39-3ec3-4fe3-8997-b76aa0bfb400',
@ -68,6 +69,7 @@ export const seedComments = async (prisma: PrismaClient) => {
update: {}, update: {},
create: { create: {
id: 'twenty-fe256b39-3ec3-4fe3-8997-a76aa0bfb600', id: 'twenty-fe256b39-3ec3-4fe3-8997-a76aa0bfb600',
workspaceId: 'twenty-7ed9d212-1c25-4d02-bf25-6aeccf7ea419',
commentableType: 'Person', commentableType: 'Person',
commentableId: 'twenty-755035db-623d-41fe-92e7-dd45b7c568e1', commentableId: 'twenty-755035db-623d-41fe-92e7-dd45b7c568e1',
commentThreadId: 'twenty-fe256b39-3ec3-4fe3-8997-b76aa0bfc408', commentThreadId: 'twenty-fe256b39-3ec3-4fe3-8997-b76aa0bfc408',
@ -103,6 +105,7 @@ export const seedComments = async (prisma: PrismaClient) => {
update: {}, update: {},
create: { create: {
id: 'twenty-dev-fe256b39-3ec3-4fe3-8997-a76aa0bfba00', id: 'twenty-dev-fe256b39-3ec3-4fe3-8997-a76aa0bfba00',
workspaceId: 'twenty-dev-7ed9d212-1c25-4d02-bf25-6aeccf7ea420',
commentableType: 'Company', commentableType: 'Company',
commentableId: 'twenty-dev-a674fa6c-1455-4c57-afaf-dd5dc086361e', commentableId: 'twenty-dev-a674fa6c-1455-4c57-afaf-dd5dc086361e',
commentThreadId: 'twenty-dev-fe256b39-3ec3-4fe3-8997-b76aaabfb408', commentThreadId: 'twenty-dev-fe256b39-3ec3-4fe3-8997-b76aaabfb408',

View File

@ -1,106 +0,0 @@
import {
CanActivate,
ExecutionContext,
HttpException,
HttpStatus,
Injectable,
} from '@nestjs/common';
import { GqlExecutionContext } from '@nestjs/graphql';
import { PrismaService } from 'src/database/prisma.service';
@Injectable()
export class CreateOneCommentThreadGuard implements CanActivate {
constructor(private prismaService: PrismaService) {}
async canActivate(context: ExecutionContext): Promise<boolean> {
const gqlContext = GqlExecutionContext.create(context);
// TODO: type request
const request = gqlContext.getContext().req;
const args = gqlContext.getArgs();
const targets = args.data?.commentThreadTargets?.createMany?.data;
const comments = args.data?.comments?.createMany?.data;
const workspace = request.user.workspace;
if (!targets || targets.length === 0) {
throw new HttpException(
{ reason: 'Missing commentThreadTargets' },
HttpStatus.BAD_REQUEST,
);
}
await targets.map(async (target) => {
if (!target.commentableId || !target.commentableType) {
throw new HttpException(
{
reason:
'Missing commentThreadTarget.commentableId or commentThreadTarget.commentableType',
},
HttpStatus.BAD_REQUEST,
);
}
if (!['Person', 'Company'].includes(target.commentableType)) {
throw new HttpException(
{ reason: 'Invalid commentThreadTarget.commentableType' },
HttpStatus.BAD_REQUEST,
);
}
const targetEntity = await this.prismaService[
target.commentableType
].findUnique({
where: { id: target.commentableId },
});
if (!targetEntity || targetEntity.workspaceId !== workspace.id) {
throw new HttpException(
{ reason: 'CommentThreadTarget not found' },
HttpStatus.NOT_FOUND,
);
}
});
if (!comments) {
return true;
}
await comments.map(async (comment) => {
if (!comment.authorId) {
throw new HttpException(
{ reason: 'Missing comment.authorId' },
HttpStatus.BAD_REQUEST,
);
}
const author = await this.prismaService.user.findUnique({
where: { id: comment.authorId },
});
if (!author) {
throw new HttpException(
{ reason: 'Comment.authorId not found' },
HttpStatus.NOT_FOUND,
);
}
const userWorkspaceMember =
await this.prismaService.workspaceMember.findFirst({
where: { userId: author.id },
});
if (
!userWorkspaceMember ||
userWorkspaceMember.workspaceId !== workspace.id
) {
throw new HttpException(
{ reason: 'userWorkspaceMember.workspaceId not found' },
HttpStatus.NOT_FOUND,
);
}
});
return true;
}
}

View File

@ -1,72 +0,0 @@
import {
CanActivate,
ExecutionContext,
HttpException,
HttpStatus,
Injectable,
} from '@nestjs/common';
import { GqlExecutionContext } from '@nestjs/graphql';
import { PrismaService } from 'src/database/prisma.service';
@Injectable()
export class CreateOneCommentGuard implements CanActivate {
constructor(private prismaService: PrismaService) {}
async canActivate(context: ExecutionContext): Promise<boolean> {
const gqlContext = GqlExecutionContext.create(context);
const request = gqlContext.getContext().req;
const args = gqlContext.getArgs();
const authorId = args.data?.author?.connect?.id;
const commentThreadId = args.data?.commentThread?.connect?.id;
if (!authorId || !commentThreadId) {
throw new HttpException(
{ reason: 'Missing author or commentThread' },
HttpStatus.BAD_REQUEST,
);
}
const author = await this.prismaService.user.findUnique({
where: { id: authorId },
});
const commentThread = await this.prismaService.commentThread.findUnique({
where: { id: commentThreadId },
});
if (!author || !commentThread) {
throw new HttpException(
{ reason: 'Author or commentThread not found' },
HttpStatus.NOT_FOUND,
);
}
const userWorkspaceMember =
await this.prismaService.workspaceMember.findFirst({
where: { userId: author.id },
});
if (!userWorkspaceMember) {
throw new HttpException(
{ reason: 'Author or commentThread not found' },
HttpStatus.NOT_FOUND,
);
}
const workspace = request.user.workspace;
if (
userWorkspaceMember.workspaceId !== workspace.id ||
commentThread.workspaceId !== workspace.id
) {
throw new HttpException(
{ reason: 'Author or commentThread not found' },
HttpStatus.NOT_FOUND,
);
}
return true;
}
}

View File

@ -1,13 +0,0 @@
import { CanActivate, Injectable } from '@nestjs/common';
import { PrismaService } from 'src/database/prisma.service';
@Injectable()
export class CreateOneGuard implements CanActivate {
constructor(private prismaService: PrismaService) {}
async canActivate(): Promise<boolean> {
// TODO
return true;
}
}

View File

@ -1,13 +0,0 @@
import { CanActivate, Injectable } from '@nestjs/common';
import { PrismaService } from 'src/database/prisma.service';
@Injectable()
export class DeleteManyGuard implements CanActivate {
constructor(private prismaService: PrismaService) {}
async canActivate(): Promise<boolean> {
// TODO
return true;
}
}

View File

@ -1,50 +0,0 @@
import {
CanActivate,
ExecutionContext,
HttpException,
HttpStatus,
Injectable,
} from '@nestjs/common';
import { GqlExecutionContext } from '@nestjs/graphql';
import { PrismaService } from 'src/database/prisma.service';
@Injectable()
export class UpdateOneGuard implements CanActivate {
constructor(private prismaService: PrismaService) {}
async canActivate(context: ExecutionContext): Promise<boolean> {
const gqlContext = GqlExecutionContext.create(context);
const request = gqlContext.getContext().req;
const entity = gqlContext.getArgByIndex(3).returnType?.name;
const args = gqlContext.getArgs();
if (!entity || !args.where?.id) {
throw new HttpException(
{ reason: 'Invalid Request' },
HttpStatus.BAD_REQUEST,
);
}
const object = await this.prismaService[entity].findUniqueOrThrow({
where: { id: args.where.id },
});
if (!object) {
throw new HttpException(
{ reason: 'Record not found' },
HttpStatus.NOT_FOUND,
);
}
const workspace = request.user.workspace;
if (object.workspaceId !== workspace.id) {
throw new HttpException(
{ reason: 'Record not found' },
HttpStatus.NOT_FOUND,
);
}
return true;
}
}

View File

@ -15,7 +15,7 @@ export class PrismaHealthIndicator extends HealthIndicator {
async isDatabaseInstanceHealthy(key: string): Promise<HealthIndicatorResult> { async isDatabaseInstanceHealthy(key: string): Promise<HealthIndicatorResult> {
try { try {
await this.prismaService.$queryRaw`SELECT 1`; await this.prismaService.client.$queryRaw`SELECT 1`;
return this.getStatus(key, true); return this.getStatus(key, true);
} catch (e) { } catch (e) {
throw new HealthCheckError('Prisma check failed', e); throw new HealthCheckError('Prisma check failed', e);

View File

@ -1262,9 +1262,9 @@
dependencies: dependencies:
"@ucast/mongo2js" "^1.3.0" "@ucast/mongo2js" "^1.3.0"
"@casl/prisma@^1.4.0": "@casl/prisma@1.4.0":
version "1.4.0" version "1.4.0"
resolved "https://registry.npmjs.org/@casl/prisma/-/prisma-1.4.0.tgz" resolved "https://registry.yarnpkg.com/@casl/prisma/-/prisma-1.4.0.tgz#0b446e272c2b1ab300de8958c39aa534e3a29db2"
integrity sha512-edDoBfm2aSww5HLyAqKmSYlGqCX06Bo8j+4P8hBNuIxmSO97Q1jEO8hkCzMThnucuGFEbNvUct1+K64CH2zTWQ== integrity sha512-edDoBfm2aSww5HLyAqKmSYlGqCX06Bo8j+4P8hBNuIxmSO97Q1jEO8hkCzMThnucuGFEbNvUct1+K64CH2zTWQ==
dependencies: dependencies:
"@ucast/core" "^1.10.0" "@ucast/core" "^1.10.0"
@ -1945,12 +1945,12 @@
"@paljs/types" "5.3.3" "@paljs/types" "5.3.3"
"@prisma/internals" "^4.15.0" "@prisma/internals" "^4.15.0"
"@prisma/client@^4.13.0": "@prisma/client@4.13.0":
version "4.15.0" version "4.13.0"
resolved "https://registry.npmjs.org/@prisma/client/-/client-4.15.0.tgz" resolved "https://registry.yarnpkg.com/@prisma/client/-/client-4.13.0.tgz#271d2b9756503ea17bbdb459c7995536cf2a6191"
integrity sha512-xnROvyABcGiwqRNdrObHVZkD9EjkJYHOmVdlKy1yGgI+XOzvMzJ4tRg3dz1pUlsyhKxXGCnjIQjWW+2ur+YXuw== integrity sha512-YaiiICcRB2hatxsbnfB66uWXjcRw3jsZdlAVxmx0cFcTc/Ad/sKdHCcWSnqyDX47vAewkjRFwiLwrOUjswVvmA==
dependencies: dependencies:
"@prisma/engines-version" "4.15.0-28.8fbc245156db7124f997f4cecdd8d1219e360944" "@prisma/engines-version" "4.13.0-50.1e7af066ee9cb95cf3a403c78d9aab3e6b04f37a"
"@prisma/debug@4.15.0": "@prisma/debug@4.15.0":
version "4.15.0" version "4.15.0"
@ -1970,15 +1970,15 @@
debug "4.3.4" debug "4.3.4"
strip-ansi "6.0.1" strip-ansi "6.0.1"
"@prisma/engines-version@4.15.0-28.8fbc245156db7124f997f4cecdd8d1219e360944": "@prisma/engines-version@4.13.0-50.1e7af066ee9cb95cf3a403c78d9aab3e6b04f37a":
version "4.15.0-28.8fbc245156db7124f997f4cecdd8d1219e360944" version "4.13.0-50.1e7af066ee9cb95cf3a403c78d9aab3e6b04f37a"
resolved "https://registry.npmjs.org/@prisma/engines-version/-/engines-version-4.15.0-28.8fbc245156db7124f997f4cecdd8d1219e360944.tgz" resolved "https://registry.yarnpkg.com/@prisma/engines-version/-/engines-version-4.13.0-50.1e7af066ee9cb95cf3a403c78d9aab3e6b04f37a.tgz#ae338908d11685dee50e7683502d75442b955bf9"
integrity sha512-sVOig4tjGxxlYaFcXgE71f/rtFhzyYrfyfNFUsxCIEJyVKU9rdOWIlIwQ2NQ7PntvGnn+x0XuFo4OC1jvPJKzg== integrity sha512-fsQlbkhPJf08JOzKoyoD9atdUijuGBekwoOPZC3YOygXEml1MTtgXVpnUNchQlRSY82OQ6pSGQ9PxUe4arcSLQ==
"@prisma/engines@4.15.0": "@prisma/engines@4.13.0":
version "4.15.0" version "4.13.0"
resolved "https://registry.npmjs.org/@prisma/engines/-/engines-4.15.0.tgz" resolved "https://registry.yarnpkg.com/@prisma/engines/-/engines-4.13.0.tgz#582a6b90b6efeb0f465984f1fe0e72a4afaaa5ae"
integrity sha512-FTaOCGs0LL0OW68juZlGxFtYviZa4xdQj/rQEdat2txw0s3Vu/saAPKjNVXfIgUsGXmQ72HPgNr6935/P8FNAA== integrity sha512-HrniowHRZXHuGT9XRgoXEaP2gJLXM5RMoItaY2PkjvuZ+iHc0Zjbm/302MB8YsPdWozAPHHn+jpFEcEn71OgPw==
"@prisma/engines@4.16.0": "@prisma/engines@4.16.0":
version "4.16.0" version "4.16.0"
@ -7415,7 +7415,7 @@ pretty-format@^28.0.0, pretty-format@^28.1.3:
prisma-nestjs-graphql@^18.0.2: prisma-nestjs-graphql@^18.0.2:
version "18.0.2" version "18.0.2"
resolved "https://registry.npmjs.org/prisma-nestjs-graphql/-/prisma-nestjs-graphql-18.0.2.tgz" resolved "https://registry.yarnpkg.com/prisma-nestjs-graphql/-/prisma-nestjs-graphql-18.0.2.tgz#852b9386d2c26bad0bd82254a5cc2e483a96d5b5"
integrity sha512-Gh7DDXyzJQBKWkQwKOpSsXygxNkfUO+Bq7q79HZgoKa+/oUzi84Fh4bjzU1REki9635rmZC8F5MoVNTzKaUrnw== integrity sha512-Gh7DDXyzJQBKWkQwKOpSsXygxNkfUO+Bq7q79HZgoKa+/oUzi84Fh4bjzU1REki9635rmZC8F5MoVNTzKaUrnw==
dependencies: dependencies:
"@prisma/generator-helper" "^4.14.0" "@prisma/generator-helper" "^4.14.0"
@ -7438,12 +7438,12 @@ prisma-query-log@^3.2.0:
dependencies: dependencies:
"@sqltools/formatter" "^1.2.3" "@sqltools/formatter" "^1.2.3"
prisma@^4.13.0: prisma@4.13.0:
version "4.15.0" version "4.13.0"
resolved "https://registry.npmjs.org/prisma/-/prisma-4.15.0.tgz" resolved "https://registry.yarnpkg.com/prisma/-/prisma-4.13.0.tgz#0b83f40acf50cd47d7463a135c4e9b275713e602"
integrity sha512-iKZZpobPl48gTcSZVawLMQ3lEy6BnXwtoMj7hluoGFYu2kQ6F9LBuBrUyF95zRVnNo8/3KzLXJXJ5TEnLSJFiA== integrity sha512-L9mqjnSmvWIRCYJ9mQkwCtj4+JDYYTdhoyo8hlsHNDXaZLh/b4hR0IoKIBbTKxZuyHQzLopb/+0Rvb69uGV7uA==
dependencies: dependencies:
"@prisma/engines" "4.15.0" "@prisma/engines" "4.13.0"
process-nextick-args@~2.0.0: process-nextick-args@~2.0.0:
version "2.0.1" version "2.0.1"