[permissions] QA fixes (#13698)

In this PR

- Introduction of readableFields and updatableFields in
objectMetadataItem selector to ease filtering from a developer
experience perspective ( + to help developers think to do it). In
discussion @lucasbordeau @charlesBochet
- Remove non-updatable field from CSV import process (@etiennejouan)
- QA fix / Non-readable fields should not show on show page
- QA fix / It should not be offered to create a kanban view on a
non-readable field
- QA fix / It should not be offered to create view groups on a
non-readable field
- QA fix / Rating field should have a readonly mode

---------

Co-authored-by: Charles Bochet <charles@twenty.com>
This commit is contained in:
Marie
2025-08-07 18:21:59 +02:00
committed by GitHub
parent 258f21a4be
commit ee19ee5119
120 changed files with 1211 additions and 943 deletions
@@ -4,7 +4,7 @@ import { useRecoilValue } from 'recoil';
import { currentUserState } from '@/auth/states/currentUserState';
import { currentWorkspaceState } from '@/auth/states/currentWorkspaceState';
import { useLoadMockedObjectMetadataItems } from '@/object-metadata/hooks/useLoadMockedObjectMetadataItems';
import { useRefreshObjectMetadataItems } from '@/object-metadata/hooks/useRefreshObjectMetadataItem';
import { useRefreshObjectMetadataItems } from '@/object-metadata/hooks/useRefreshObjectMetadataItems';
import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState';
import { isWorkspaceActiveOrSuspended } from 'twenty-shared/workspace';
import { isUndefinedOrNull } from '~/utils/isUndefinedOrNull';
@@ -109,97 +109,6 @@ export const queries = {
}
}
`,
getCurrentUser: gql`
query GetCurrentUser {
currentUser {
...UserQueryFragment
}
}
fragment UserQueryFragment on User {
id
firstName
lastName
email
canAccessFullAdminPanel
canImpersonate
supportUserHash
onboardingStatus
workspaceMember {
...WorkspaceMemberQueryFragment
}
workspaceMembers {
...WorkspaceMemberQueryFragment
}
currentUserWorkspace {
permissionFlags
objectRecordsPermissions
}
currentWorkspace {
id
displayName
logo
inviteHash
allowImpersonation
activationStatus
isPublicInviteLinkEnabled
isGoogleAuthEnabled
isMicrosoftAuthEnabled
isPasswordAuthEnabled
subdomain
hasValidEnterpriseKey
customDomain
workspaceUrls {
...WorkspaceUrlsFragment
}
featureFlags {
id
key
value
workspaceId
}
metadataVersion
currentBillingSubscription {
id
status
interval
}
billingSubscriptions {
id
status
}
workspaceMembersCount
}
workspaces {
workspace {
id
logo
displayName
subdomain
customDomain
workspaceUrls {
...WorkspaceUrlsFragment
}
}
}
userVars
}
fragment WorkspaceMemberQueryFragment on WorkspaceMember {
id
name {
firstName
lastName
}
colorScheme
avatarUrl
locale
userEmail
timeZone
dateFormat
timeFormat
}
`,
};
export const objectMetadataId = '25611fce-6637-4089-b0ca-91afeec95784';
@@ -14,6 +14,8 @@ import {
query as findManyObjectMetadataItemsQuery,
responseData as findManyObjectMetadataItemsResponseData,
} from '../__mocks__/useFindManyObjectMetadataItems';
import { GET_CURRENT_USER } from '@/users/graphql/queries/getCurrentUser';
import { mockedUserData } from '~/testing/mock-data/users';
const mocks = [
{
@@ -27,6 +29,17 @@ const mocks = [
},
})),
},
{
request: {
query: GET_CURRENT_USER,
variables: {},
},
result: jest.fn(() => ({
data: {
currentUser: mockedUserData,
},
})),
},
{
request: {
query: findManyObjectMetadataItemsQuery,
@@ -11,10 +11,12 @@ import {
variables,
} from '../__mocks__/useDeleteOneObjectMetadataItem';
import { GET_CURRENT_USER } from '@/users/graphql/queries/getCurrentUser';
import {
query as findManyObjectMetadataItemsQuery,
responseData as findManyObjectMetadataItemsResponseData,
} from '../__mocks__/useFindManyObjectMetadataItems';
import { mockedUserData } from '~/testing/mock-data/users';
const mocks = [
{
@@ -28,6 +30,17 @@ const mocks = [
},
})),
},
{
request: {
query: GET_CURRENT_USER,
variables: {},
},
result: jest.fn(() => ({
data: {
currentUser: mockedUserData,
},
})),
},
{
request: {
query: findManyObjectMetadataItemsQuery,
@@ -15,6 +15,8 @@ import {
variables,
} from '../__mocks__/useFieldMetadataItem';
import { GET_CURRENT_USER } from '@/users/graphql/queries/getCurrentUser';
import { mockedUserData } from '~/testing/mock-data/users';
import {
query as findManyObjectMetadataItemsQuery,
responseData as findManyObjectMetadataItemsResponseData,
@@ -97,6 +99,17 @@ const mocks = [
},
})),
},
{
request: {
query: GET_CURRENT_USER,
variables: {},
},
result: jest.fn(() => ({
data: {
currentUser: mockedUserData,
},
})),
},
{
request: {
query: queries.deleteMetadataField,
@@ -130,15 +143,6 @@ const mocks = [
},
})),
},
{
request: {
query: queries.getCurrentUser,
variables: {},
},
result: jest.fn(() => ({
data: responseData.getCurrentUser,
})),
},
{
request: {
query: findManyObjectMetadataItemsQuery,
@@ -9,7 +9,7 @@ export const useActiveFieldMetadataItems = ({
const activeFieldMetadataItems = useMemo(
() =>
objectMetadataItem
? objectMetadataItem.fields.filter(
? objectMetadataItem.readableFields.filter(
({ isActive, isSystem }) => isActive && !isSystem,
)
: [],
@@ -5,17 +5,13 @@ import { filterAvailableTableColumns } from '@/object-record/utils/filterAvailab
import { availableFieldMetadataItemsForFilterFamilySelector } from '@/object-metadata/states/availableFieldMetadataItemsForFilterFamilySelector';
import { availableFieldMetadataItemsForSortFamilySelector } from '@/object-metadata/states/availableFieldMetadataItemsForSortFamilySelector';
import { getReadRestrictedFieldMetadataIdsFromObjectPermissions } from '@/object-metadata/utils/getReadRestrictedFieldMetadataIdsFromObjectPermissions';
import { useObjectPermissions } from '@/object-record/hooks/useObjectPermissions';
import { useFeatureFlagsMap } from '@/workspace/hooks/useFeatureFlagsMap';
import { useRecoilValue } from 'recoil';
import { FeatureFlagKey } from '~/generated/graphql';
import { formatFieldMetadataItemAsColumnDefinition } from '../utils/formatFieldMetadataItemAsColumnDefinition';
export const useColumnDefinitionsFromFieldMetadata = (
objectMetadataItem: ObjectMetadataItem,
) => {
const activeFieldMetadataItems = objectMetadataItem.fields.filter(
const activeFieldMetadataItems = objectMetadataItem.readableFields.filter(
({ isActive, isSystem }) => isActive && !isSystem,
);
@@ -31,23 +27,7 @@ export const useColumnDefinitionsFromFieldMetadata = (
}),
);
const { objectPermissionsByObjectMetadataId } = useObjectPermissions();
let restrictedFieldMetadataIds: string[] = [];
const featureFlags = useFeatureFlagsMap();
const isFieldsPermissionsEnabled =
featureFlags[FeatureFlagKey.IS_FIELDS_PERMISSIONS_ENABLED];
if (isFieldsPermissionsEnabled) {
restrictedFieldMetadataIds =
getReadRestrictedFieldMetadataIdsFromObjectPermissions({
objectPermissions: [
objectPermissionsByObjectMetadataId[objectMetadataItem.id],
],
objectMetadataId: objectMetadataItem.id,
});
}
const restrictedFieldMetadataIds: string[] = [];
const columnDefinitions: ColumnDefinition<FieldMetadata>[] =
activeFieldMetadataItems
@@ -8,7 +8,7 @@ import {
import { CREATE_ONE_FIELD_METADATA_ITEM } from '../graphql/mutations';
import { useRefreshObjectMetadataItems } from '@/object-metadata/hooks/useRefreshObjectMetadataItem';
import { useRefreshObjectMetadataItems } from '@/object-metadata/hooks/useRefreshObjectMetadataItems';
import { useRefreshCachedViews } from '@/views/hooks/useRefreshViews';
export const useCreateOneFieldMetadataItem = () => {
@@ -8,7 +8,7 @@ import {
import { CREATE_ONE_OBJECT_METADATA_ITEM } from '../graphql/mutations';
import { useRefreshObjectMetadataItems } from '@/object-metadata/hooks/useRefreshObjectMetadataItem';
import { useRefreshObjectMetadataItems } from '@/object-metadata/hooks/useRefreshObjectMetadataItems';
import { useRefreshCachedViews } from '@/views/hooks/useRefreshViews';
export const useCreateOneObjectMetadataItem = () => {
@@ -6,7 +6,7 @@ import {
} from '~/generated-metadata/graphql';
import { useApolloCoreClient } from '@/object-metadata/hooks/useApolloCoreClient';
import { useRefreshObjectMetadataItems } from '@/object-metadata/hooks/useRefreshObjectMetadataItem';
import { useRefreshObjectMetadataItems } from '@/object-metadata/hooks/useRefreshObjectMetadataItems';
import { recordIndexKanbanAggregateOperationState } from '@/object-record/record-index/states/recordIndexKanbanAggregateOperationState';
import { AggregateOperations } from '@/object-record/record-table/constants/AggregateOperations';
import { useRecoilState } from 'recoil';
@@ -7,7 +7,7 @@ import {
import { DELETE_ONE_OBJECT_METADATA_ITEM } from '../graphql/mutations';
import { useRefreshObjectMetadataItems } from '@/object-metadata/hooks/useRefreshObjectMetadataItem';
import { useRefreshObjectMetadataItems } from '@/object-metadata/hooks/useRefreshObjectMetadataItems';
export const useDeleteOneObjectMetadataItem = () => {
const [mutate] = useMutation<
@@ -8,6 +8,8 @@ import {
} from '~/generated-metadata/graphql';
import { logError } from '~/utils/logError';
import { enrichObjectMetadataItemsWithPermissions } from '@/object-metadata/utils/enrichObjectMetadataItemsWithPermissions';
import { useObjectPermissions } from '@/object-record/hooks/useObjectPermissions';
import { FIND_MANY_OBJECT_METADATA_ITEMS } from '../graphql/queries';
import { mapPaginatedObjectMetadataItemsToObjectMetadataItems } from '../utils/mapPaginatedObjectMetadataItemsToObjectMetadataItems';
@@ -18,6 +20,8 @@ export const useFindManyObjectMetadataItems = ({
} = {}) => {
const { enqueueErrorSnackBar } = useSnackBar();
const { objectPermissionsByObjectMetadataId } = useObjectPermissions();
const { data, loading, error, refetch } = useQuery<
ObjectMetadataItemsQuery,
ObjectMetadataItemsQueryVariables
@@ -32,10 +36,16 @@ export const useFindManyObjectMetadataItems = ({
});
const objectMetadataItems = useMemo(() => {
return mapPaginatedObjectMetadataItemsToObjectMetadataItems({
pagedObjectMetadataItems: data,
const objectMetadataItemsArray =
mapPaginatedObjectMetadataItemsToObjectMetadataItems({
pagedObjectMetadataItems: data,
});
return enrichObjectMetadataItemsWithPermissions({
objectMetadataItems: objectMetadataItemsArray,
objectPermissionsByObjectMetadataId,
});
}, [data]);
}, [data, objectPermissionsByObjectMetadataId]);
return {
objectMetadataItems,
@@ -1,53 +0,0 @@
import { FIND_MANY_OBJECT_METADATA_ITEMS } from '@/object-metadata/graphql/queries';
import { isAppWaitingForFreshObjectMetadataState } from '@/object-metadata/states/isAppWaitingForFreshObjectMetadataState';
import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState';
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
import { mapPaginatedObjectMetadataItemsToObjectMetadataItems } from '@/object-metadata/utils/mapPaginatedObjectMetadataItemsToObjectMetadataItems';
import { FetchPolicy, useApolloClient } from '@apollo/client';
import { useRecoilCallback } from 'recoil';
import { ObjectMetadataItemsQuery } from '~/generated-metadata/graphql';
import { isDeeplyEqual } from '~/utils/isDeeplyEqual';
export const useRefreshObjectMetadataItems = (
fetchPolicy: FetchPolicy = 'network-only',
) => {
const client = useApolloClient();
const refreshObjectMetadataItems = async () => {
const result = await client.query<ObjectMetadataItemsQuery>({
query: FIND_MANY_OBJECT_METADATA_ITEMS,
variables: {},
fetchPolicy,
});
const objectMetadataItems =
mapPaginatedObjectMetadataItemsToObjectMetadataItems({
pagedObjectMetadataItems: result.data,
});
replaceObjectMetadataItemIfDifferent(objectMetadataItems);
return objectMetadataItems;
};
const replaceObjectMetadataItemIfDifferent = useRecoilCallback(
({ set, snapshot }) =>
(toSetObjectMetadataItems: ObjectMetadataItem[]) => {
if (
!isDeeplyEqual(
snapshot.getLoadable(objectMetadataItemsState).getValue(),
toSetObjectMetadataItems,
) &&
toSetObjectMetadataItems.length > 0
) {
set(objectMetadataItemsState, toSetObjectMetadataItems);
set(isAppWaitingForFreshObjectMetadataState, false);
}
},
[],
);
return {
refreshObjectMetadataItems,
};
};
@@ -0,0 +1,89 @@
import { currentUserWorkspaceState } from '@/auth/states/currentUserWorkspaceState';
import { FIND_MANY_OBJECT_METADATA_ITEMS } from '@/object-metadata/graphql/queries';
import { isAppWaitingForFreshObjectMetadataState } from '@/object-metadata/states/isAppWaitingForFreshObjectMetadataState';
import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState';
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
import { enrichObjectMetadataItemsWithPermissions } from '@/object-metadata/utils/enrichObjectMetadataItemsWithPermissions';
import { mapPaginatedObjectMetadataItemsToObjectMetadataItems } from '@/object-metadata/utils/mapPaginatedObjectMetadataItemsToObjectMetadataItems';
import { FetchPolicy, useApolloClient } from '@apollo/client';
import { useRecoilCallback } from 'recoil';
import { ObjectPermissions } from 'twenty-shared/types';
import { isDefined } from 'twenty-shared/utils';
import { ObjectMetadataItemsQuery } from '~/generated-metadata/graphql';
import { isDeeplyEqual } from '~/utils/isDeeplyEqual';
export const useRefreshObjectMetadataItems = (
fetchPolicy: FetchPolicy = 'network-only',
) => {
const client = useApolloClient();
const refreshObjectMetadataItems = async () => {
const objectMetadataItemsResult =
await client.query<ObjectMetadataItemsQuery>({
query: FIND_MANY_OBJECT_METADATA_ITEMS,
variables: {},
fetchPolicy,
});
const objectMetadataItems =
mapPaginatedObjectMetadataItemsToObjectMetadataItems({
pagedObjectMetadataItems: objectMetadataItemsResult.data,
});
return replaceObjectMetadataItemIfDifferent(objectMetadataItems);
};
const replaceObjectMetadataItemIfDifferent = useRecoilCallback(
({ set, snapshot }) =>
(
toSetObjectMetadataItems: Omit<
ObjectMetadataItem,
'readableFields' | 'updatableFields'
>[],
) => {
const currentUserWorkspace = snapshot
.getLoadable(currentUserWorkspaceState)
.getValue();
if (!isDefined(currentUserWorkspace)) {
return;
}
const objectPermissionsByObjectMetadataId =
currentUserWorkspace.objectPermissions.reduce(
(acc, objectPermission) => {
acc[objectPermission.objectMetadataId] = objectPermission;
return acc;
},
{} as Record<
string,
ObjectPermissions & { objectMetadataId: string }
>,
);
const newObjectMetadataItems = enrichObjectMetadataItemsWithPermissions(
{
objectMetadataItems: toSetObjectMetadataItems,
objectPermissionsByObjectMetadataId,
},
);
if (
!isDeeplyEqual(
snapshot.getLoadable(objectMetadataItemsState).getValue(),
newObjectMetadataItems,
) &&
newObjectMetadataItems.length > 0
) {
set(objectMetadataItemsState, newObjectMetadataItems);
set(isAppWaitingForFreshObjectMetadataState, false);
}
return newObjectMetadataItems;
},
[],
);
return {
refreshObjectMetadataItems,
};
};
@@ -9,7 +9,7 @@ import { UPDATE_ONE_FIELD_METADATA_ITEM } from '../graphql/mutations';
import { currentWorkspaceState } from '@/auth/states/currentWorkspaceState';
import { useApolloCoreClient } from '@/object-metadata/hooks/useApolloCoreClient';
import { useRefreshObjectMetadataItems } from '@/object-metadata/hooks/useRefreshObjectMetadataItem';
import { useRefreshObjectMetadataItems } from '@/object-metadata/hooks/useRefreshObjectMetadataItems';
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
import { useFindManyRecordsQuery } from '@/object-record/hooks/useFindManyRecordsQuery';
import { GET_CURRENT_USER } from '@/users/graphql/queries/getCurrentUser';
@@ -8,7 +8,7 @@ import {
import { UPDATE_ONE_OBJECT_METADATA_ITEM } from '../graphql/mutations';
import { useRefreshObjectMetadataItems } from '@/object-metadata/hooks/useRefreshObjectMetadataItem';
import { useRefreshObjectMetadataItems } from '@/object-metadata/hooks/useRefreshObjectMetadataItems';
// TODO: Slice the Apollo store synchronously in the update function instead of subscribing, so we can use update after read in the same function call
export const useUpdateOneObjectMetadataItem = () => {
@@ -1,8 +1,6 @@
import { currentUserWorkspaceState } from '@/auth/states/currentUserWorkspaceState';
import { currentWorkspaceState } from '@/auth/states/currentWorkspaceState';
import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState';
import { getFilterFilterableFieldMetadataItems } from '@/object-metadata/utils/getFilterFilterableFieldMetadataItems';
import { getReadRestrictedFieldMetadataIdsFromObjectPermissions } from '@/object-metadata/utils/getReadRestrictedFieldMetadataIdsFromObjectPermissions';
import { checkIfFeatureFlagIsEnabledOnWorkspace } from '@/workspace/utils/checkIfFeatureFlagIsEnabledOnWorkspace';
import { selectorFamily } from 'recoil';
import { isDefined } from 'twenty-shared/utils';
@@ -29,34 +27,15 @@ export const availableFieldMetadataItemsForFilterFamilySelector =
currentWorkspace,
);
const isFieldsPermissionsEnabled =
checkIfFeatureFlagIsEnabledOnWorkspace(
FeatureFlagKey.IS_FIELDS_PERMISSIONS_ENABLED,
currentWorkspace,
);
const filterFilterableFieldMetadataItems =
getFilterFilterableFieldMetadataItems({
isJsonFilterEnabled: isJsonFeatureFlagEnabled,
});
let restrictedFieldMetadataIds: string[] = [];
if (isFieldsPermissionsEnabled) {
const currentUserWorkspace = get(currentUserWorkspaceState);
restrictedFieldMetadataIds =
getReadRestrictedFieldMetadataIdsFromObjectPermissions({
objectPermissions: currentUserWorkspace?.objectPermissions,
objectMetadataId: objectMetadataItem.id,
});
}
const availableFieldMetadataItemsForFilter = objectMetadataItem.fields
.filter(filterFilterableFieldMetadataItems)
.filter((field) => {
return !restrictedFieldMetadataIds.includes(field.id);
});
const availableFieldMetadataItemsForFilter =
objectMetadataItem.readableFields.filter(
filterFilterableFieldMetadataItems,
);
return availableFieldMetadataItemsForFilter;
},
});
@@ -1,12 +1,7 @@
import { currentUserWorkspaceState } from '@/auth/states/currentUserWorkspaceState';
import { currentWorkspaceState } from '@/auth/states/currentWorkspaceState';
import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState';
import { filterSortableFieldMetadataItems } from '@/object-metadata/utils/filterSortableFieldMetadataItems';
import { getReadRestrictedFieldMetadataIdsFromObjectPermissions } from '@/object-metadata/utils/getReadRestrictedFieldMetadataIdsFromObjectPermissions';
import { checkIfFeatureFlagIsEnabledOnWorkspace } from '@/workspace/utils/checkIfFeatureFlagIsEnabledOnWorkspace';
import { selectorFamily } from 'recoil';
import { isDefined } from 'twenty-shared/utils';
import { FeatureFlagKey } from '~/generated/graphql';
export const availableFieldMetadataItemsForSortFamilySelector = selectorFamily({
key: 'availableFieldMetadataItemsForSortFamilySelector',
@@ -23,28 +18,10 @@ export const availableFieldMetadataItemsForSortFamilySelector = selectorFamily({
return [];
}
const currentWorkspace = get(currentWorkspaceState);
const isFieldsPermissionsEnabled = checkIfFeatureFlagIsEnabledOnWorkspace(
FeatureFlagKey.IS_FIELDS_PERMISSIONS_ENABLED,
currentWorkspace,
);
let restrictedFieldMetadataIds: string[] = [];
if (isFieldsPermissionsEnabled) {
const currentUserWorkspace = get(currentUserWorkspaceState);
restrictedFieldMetadataIds =
getReadRestrictedFieldMetadataIdsFromObjectPermissions({
objectPermissions: currentUserWorkspace?.objectPermissions,
objectMetadataId: objectMetadataItem.id,
});
}
const availableFieldMetadataItemsForSort = objectMetadataItem.fields
.filter(filterSortableFieldMetadataItems)
.filter((field) => {
return !restrictedFieldMetadataIds.includes(field.id);
});
const availableFieldMetadataItemsForSort =
objectMetadataItem.readableFields.filter(
filterSortableFieldMetadataItems,
);
return availableFieldMetadataItemsForSort;
},
@@ -15,6 +15,8 @@ export type ObjectMetadataItem = Omit<
> & {
__typename?: string;
fields: FieldMetadataItem[];
readableFields: FieldMetadataItem[];
updatableFields: FieldMetadataItem[];
labelIdentifierFieldMetadataId: string;
indexMetadatas: IndexMetadataItem[];
};
@@ -37,6 +37,7 @@ describe('mapObjectMetadataToGraphQLQuery', () => {
canSoftDeleteObjectRecords: true,
canDestroyObjectRecords: true,
objectMetadataId: personObjectMetadataItem.id,
restrictedFields: {},
},
},
});
@@ -140,6 +141,7 @@ describe('mapObjectMetadataToGraphQLQuery', () => {
canSoftDeleteObjectRecords: true,
canDestroyObjectRecords: true,
objectMetadataId: personObjectMetadataItem.id,
restrictedFields: {},
},
},
});
@@ -0,0 +1,57 @@
import { getNonReadableFieldMetadataIdsFromObjectPermissions } from '@/object-metadata/utils/getNonReadableFieldMetadataIdsFromObjectPermissions';
import { getNonUpdatableFieldMetadataIdsFromObjectPermissions } from '@/object-metadata/utils/getNonUpdatableFieldMetadataIdsFromObjectPermissions';
import { getObjectPermissionsFromMapByObjectMetadataId } from '@/settings/roles/role-permissions/objects-permissions/utils/getObjectPermissionsFromMapByObjectMetadataId';
import { ObjectPermissions } from 'twenty-shared/types';
import { isDefined } from 'twenty-shared/utils';
import { ObjectMetadataItem } from '../types/ObjectMetadataItem';
type enrichObjectMetadataItemsWithPermissionsArgs = {
objectMetadataItems: Omit<
ObjectMetadataItem,
'readableFields' | 'updatableFields'
>[];
objectPermissionsByObjectMetadataId: Record<
string,
ObjectPermissions & { objectMetadataId: string }
>;
};
export const enrichObjectMetadataItemsWithPermissions = ({
objectMetadataItems,
objectPermissionsByObjectMetadataId,
}: enrichObjectMetadataItemsWithPermissionsArgs) => {
const formattedObjects: ObjectMetadataItem[] =
objectMetadataItems.map((object) => {
const objectPermissions = getObjectPermissionsFromMapByObjectMetadataId({
objectPermissionsByObjectMetadataId,
objectMetadataId: object.id,
});
const nonReadableFieldMetadataIds = !isDefined(objectPermissions)
? []
: getNonReadableFieldMetadataIdsFromObjectPermissions({
objectPermissions: objectPermissions,
});
const nonUpdatableFieldMetadataIds = !isDefined(objectPermissions)
? []
: getNonUpdatableFieldMetadataIdsFromObjectPermissions({
objectPermissions: objectPermissions,
});
const { fields, ...objectWithoutFields } = object;
return {
...objectWithoutFields,
fields: fields,
readableFields: fields.filter(
(field) => !nonReadableFieldMetadataIds.includes(field.id),
),
updatableFields: fields.filter(
(field) => !nonUpdatableFieldMetadataIds.includes(field.id),
),
} satisfies ObjectMetadataItem;
}) ?? [];
return formattedObjects;
};
@@ -1,9 +1,9 @@
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
export const getActiveFieldMetadataItems = (
objectMetadataItem: Pick<ObjectMetadataItem, 'fields'>,
objectMetadataItem: Pick<ObjectMetadataItem, 'readableFields'>,
) =>
objectMetadataItem.fields.filter(
objectMetadataItem.readableFields.filter(
(fieldMetadataItem) =>
fieldMetadataItem.isActive && !fieldMetadataItem.isSystem,
);
@@ -0,0 +1,15 @@
import { ObjectPermissions } from 'twenty-shared/types';
type GetNonReadableFieldMetadataIdsFromObjectPermissionsArgs = {
objectPermissions: ObjectPermissions;
};
export const getNonReadableFieldMetadataIdsFromObjectPermissions = ({
objectPermissions,
}: GetNonReadableFieldMetadataIdsFromObjectPermissionsArgs) => {
const restrictedFields = objectPermissions.restrictedFields;
return Object.entries(restrictedFields)
.filter(([_, restrictedField]) => restrictedField.canRead === false)
.map(([fieldMetadataId]) => fieldMetadataId);
};
@@ -0,0 +1,15 @@
import { ObjectPermissions } from 'twenty-shared/types';
type GetNonUpdatableFieldMetadataIdsFromObjectPermissionsArgs = {
objectPermissions: ObjectPermissions;
};
export const getNonUpdatableFieldMetadataIdsFromObjectPermissions = ({
objectPermissions,
}: GetNonUpdatableFieldMetadataIdsFromObjectPermissionsArgs): string[] => {
const restrictedFields = objectPermissions.restrictedFields;
return Object.entries(restrictedFields)
.filter(([_, restrictedField]) => restrictedField.canUpdate === false)
.map(([fieldMetadataId]) => fieldMetadataId);
};
@@ -1,12 +1,18 @@
import { getObjectPermissionsFromMapByObjectMetadataId } from '@/settings/roles/role-permissions/objects-permissions/utils/getObjectPermissionsFromMapByObjectMetadataId';
import { ObjectPermissions } from 'twenty-shared/types';
import { isDefined } from 'twenty-shared/utils';
import { ObjectPermission } from '~/generated/graphql';
export const getObjectPermissionsForObject = (
objectPermissionsByObjectMetadataId: Record<string, ObjectPermission>,
objectPermissionsByObjectMetadataId: Record<
string,
ObjectPermissions & { objectMetadataId: string }
>,
objectMetadataId: string,
): ObjectPermission => {
const objectPermissions =
objectPermissionsByObjectMetadataId[objectMetadataId];
): ObjectPermissions & { objectMetadataId: string } => {
const objectPermissions = getObjectPermissionsFromMapByObjectMetadataId({
objectPermissionsByObjectMetadataId,
objectMetadataId,
});
if (!isDefined(objectPermissions)) {
return {
@@ -1,21 +0,0 @@
import { RestrictedField } from 'twenty-shared/types';
import { ObjectPermission } from '~/generated/graphql';
export const getReadRestrictedFieldMetadataIdsFromObjectPermissions = ({
objectPermissions,
objectMetadataId,
}: {
objectPermissions?: ObjectPermission[] | null;
objectMetadataId: string;
}) => {
const restrictedFields = objectPermissions?.find(
(permission) => permission.objectMetadataId === objectMetadataId,
)?.restrictedFields;
return Object.entries(restrictedFields ?? {})
.filter(
([_fieldMetadataId, restrictedField]) =>
(restrictedField as RestrictedField).canRead === false,
)
.map(([fieldMetadataId]) => fieldMetadataId);
};
@@ -1,10 +1,6 @@
import { mapObjectMetadataToGraphQLQuery } from '@/object-metadata/utils/mapObjectMetadataToGraphQLQuery';
import { isUndefined } from '@sniptt/guards';
import {
FieldMetadataType,
ObjectPermission,
RelationType,
} from '~/generated-metadata/graphql';
import { FieldMetadataType, RelationType } from '~/generated-metadata/graphql';
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
import { getObjectPermissionsForObject } from '@/object-metadata/utils/getObjectPermissionsForObject';
@@ -12,6 +8,7 @@ import { RecordGqlFields } from '@/object-record/graphql/types/RecordGqlFields';
import { isNonCompositeField } from '@/object-record/object-filter-dropdown/utils/isNonCompositeField';
import { isDefined } from 'twenty-shared/utils';
import { FieldMetadataItem } from '../types/FieldMetadataItem';
import { ObjectPermissions } from 'twenty-shared/types';
type MapFieldMetadataToGraphQLQueryArgs = {
objectMetadataItems: ObjectMetadataItem[];
@@ -22,7 +19,10 @@ type MapFieldMetadataToGraphQLQueryArgs = {
>;
relationRecordGqlFields?: RecordGqlFields;
computeReferences?: boolean;
objectPermissionsByObjectMetadataId: Record<string, ObjectPermission>;
objectPermissionsByObjectMetadataId: Record<
string,
ObjectPermissions & { objectMetadataId: string }
>;
isFieldsPermissionsEnabled?: boolean;
};
// TODO: change ObjectMetadataItems mock before refactoring with relation computed field
@@ -1,24 +1,25 @@
import { FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
import { getObjectPermissionsForObject } from '@/object-metadata/utils/getObjectPermissionsForObject';
import { mapFieldMetadataToGraphQLQuery } from '@/object-metadata/utils/mapFieldMetadataToGraphQLQuery';
import { shouldFieldBeQueried } from '@/object-metadata/utils/shouldFieldBeQueried';
import { RecordGqlFields } from '@/object-record/graphql/types/RecordGqlFields';
import { isRecordGqlFieldsNode } from '@/object-record/graphql/utils/isRecordGraphlFieldsNode';
import { FieldMetadataType } from 'twenty-shared/types';
import { FieldMetadataType, ObjectPermissions } from 'twenty-shared/types';
import { isDefined } from 'twenty-shared/utils';
import { ObjectPermission } from '~/generated/graphql';
type MapObjectMetadataToGraphQLQueryArgs = {
objectMetadataItems: ObjectMetadataItem[];
objectMetadataItem: Pick<
ObjectMetadataItem,
'nameSingular' | 'fields' | 'id'
'nameSingular' | 'fields' | 'id' | 'readableFields'
>;
recordGqlFields?: RecordGqlFields;
computeReferences?: boolean;
isRootLevel?: boolean;
objectPermissionsByObjectMetadataId: Record<string, ObjectPermission>;
objectPermissionsByObjectMetadataId: Record<
string,
ObjectPermissions & { objectMetadataId: string }
>;
isFieldsPermissionsEnabled?: boolean;
};
@@ -45,22 +46,10 @@ export const mapObjectMetadataToGraphQLQuery = ({
}
}
const filterReadableFields = (field: FieldMetadataItem) => {
if (isFieldsPermissionsEnabled !== true) {
return true;
}
const objectPermission = getObjectPermissionsForObject(
objectPermissionsByObjectMetadataId,
objectMetadataItem.id,
);
return objectPermission.restrictedFields?.[field.id]?.canRead !== false;
};
const manyToOneRelationFields = objectMetadataItem?.fields
const manyToOneRelationFields = objectMetadataItem?.readableFields
.filter((field) => field.isActive)
.filter((field) => field.type === FieldMetadataType.RELATION)
.filter((field) => isDefined(field.settings?.joinColumnName))
.filter(filterReadableFields);
.filter((field) => isDefined(field.settings?.joinColumnName));
const manyToOneRelationGqlFieldWithFieldMetadata =
manyToOneRelationFields.map((field) => ({
@@ -69,9 +58,8 @@ export const mapObjectMetadataToGraphQLQuery = ({
}));
const gqlFieldWithFieldMetadataThatCouldBeQueried = [
...objectMetadataItem.fields
...objectMetadataItem.readableFields
.filter((fieldMetadata) => fieldMetadata.isActive)
.filter(filterReadableFields)
.map((fieldMetadata) => ({
gqlField: fieldMetadata.name,
fieldMetadata,
@@ -4,12 +4,17 @@ import { objectMetadataItemSchema } from '@/object-metadata/validation-schemas/o
import { ObjectMetadataItemsQuery } from '~/generated-metadata/graphql';
import { ObjectMetadataItem } from '../types/ObjectMetadataItem';
type mapPaginatedObjectMetadataItemsToObjectMetadataItemsArgs = {
pagedObjectMetadataItems: ObjectMetadataItemsQuery | undefined;
};
export const mapPaginatedObjectMetadataItemsToObjectMetadataItems = ({
pagedObjectMetadataItems,
}: {
pagedObjectMetadataItems: ObjectMetadataItemsQuery | undefined;
}) => {
const formattedObjects: ObjectMetadataItem[] =
}: mapPaginatedObjectMetadataItemsToObjectMetadataItemsArgs) => {
const formattedObjects: Omit<
ObjectMetadataItem,
'readableFields' | 'updatableFields'
>[] =
pagedObjectMetadataItems?.objects.edges.map((object) => {
const labelIdentifierFieldMetadataId =
objectMetadataItemSchema.shape.labelIdentifierFieldMetadataId.parse(
@@ -35,7 +40,10 @@ export const mapPaginatedObjectMetadataItemsToObjectMetadataItems = ({
),
}) satisfies IndexMetadataItem,
),
} satisfies ObjectMetadataItem;
} satisfies Omit<
ObjectMetadataItem,
'readableFields' | 'updatableFields'
>;
}) ?? [];
return formattedObjects;
@@ -1,11 +1,11 @@
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
export const mapSoftDeleteFieldsToGraphQLQuery = (
objectMetadataItem: Pick<ObjectMetadataItem, 'fields'>,
objectMetadataItem: Pick<ObjectMetadataItem, 'readableFields'>,
): string => {
const softDeleteFields = ['deletedAt', 'id'];
const fieldsThatShouldBeQueried = objectMetadataItem.fields
const fieldsThatShouldBeQueried = objectMetadataItem.readableFields
.filter((field) => field.isActive && softDeleteFields.includes(field.name))
.sort(
(a, b) =>
@@ -12,6 +12,8 @@ export const objectMetadataItemSchema = z.object({
dataSourceId: z.string().uuid(),
description: z.string().trim().nullable().optional(),
fields: z.array(fieldMetadataItemSchema()),
readableFields: z.array(fieldMetadataItemSchema()),
updatableFields: z.array(fieldMetadataItemSchema()),
indexMetadatas: z.array(indexMetadataItemSchema),
icon: z.string().startsWith('Icon').trim(),
id: z.string().uuid(),