[FE] handle restricted objects 2 (#12437)

Co-authored-by: Charles Bochet <charles@twenty.com>
This commit is contained in:
Weiko
2025-06-05 15:49:22 +02:00
committed by GitHub
parent ad804ebecd
commit 3f30964523
109 changed files with 904 additions and 306 deletions

View File

@ -1,5 +1,7 @@
import { NavigationDrawerItemForObjectMetadataItem } from '@/object-metadata/components/NavigationDrawerItemForObjectMetadataItem';
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
import { getObjectPermissionsForObject } from '@/object-metadata/utils/getObjectPermissionsForObject';
import { useObjectPermissions } from '@/object-record/hooks/useObjectPermissions';
import { NavigationDrawerAnimatedCollapseWrapper } from '@/ui/navigation/navigation-drawer/components/NavigationDrawerAnimatedCollapseWrapper';
import { NavigationDrawerSection } from '@/ui/navigation/navigation-drawer/components/NavigationDrawerSection';
import { NavigationDrawerSectionTitle } from '@/ui/navigation/navigation-drawer/components/NavigationDrawerSectionTitle';
@ -27,6 +29,8 @@ export const NavigationDrawerSectionForObjectMetadataItems = ({
useNavigationSection('Objects' + (isRemote ? 'Remote' : 'Workspace'));
const isNavigationSectionOpen = useRecoilValue(isNavigationSectionOpenState);
const { objectPermissionsByObjectMetadataId } = useObjectPermissions();
const sortedStandardObjectMetadataItems = [...objectMetadataItems]
.filter((item) => ORDERED_STANDARD_OBJECTS.includes(item.nameSingular))
.sort((objectMetadataItemA, objectMetadataItemB) => {
@ -58,6 +62,15 @@ export const NavigationDrawerSectionForObjectMetadataItems = ({
...sortedCustomObjectMetadataItems,
];
const objectMetadataItemsForNavigationItemsWithReadPermission =
objectMetadataItemsForNavigationItems.filter(
(objectMetadataItem) =>
getObjectPermissionsForObject(
objectPermissionsByObjectMetadataId,
objectMetadataItem.id,
).canReadObjectRecords,
);
return (
objectMetadataItems.length > 0 && (
<NavigationDrawerSection>
@ -68,12 +81,14 @@ export const NavigationDrawerSectionForObjectMetadataItems = ({
/>
</NavigationDrawerAnimatedCollapseWrapper>
{isNavigationSectionOpen &&
objectMetadataItemsForNavigationItems.map((objectMetadataItem) => (
<NavigationDrawerItemForObjectMetadataItem
key={`navigation-drawer-item-${objectMetadataItem.id}`}
objectMetadataItem={objectMetadataItem}
/>
))}
objectMetadataItemsForNavigationItemsWithReadPermission.map(
(objectMetadataItem) => (
<NavigationDrawerItemForObjectMetadataItem
key={`navigation-drawer-item-${objectMetadataItem.id}`}
objectMetadataItem={objectMetadataItem}
/>
),
)}
</NavigationDrawerSection>
)
);

View File

@ -18,6 +18,7 @@ describe('mapFieldMetadataToGraphQLQuery', () => {
fieldMetadata: personObjectMetadataItem.fields.find(
(field) => field.name === 'id',
)!,
objectPermissionsByObjectMetadataId: {},
});
expect(normalizeGQLField(res)).toEqual(normalizeGQLField('id'));
});
@ -28,6 +29,7 @@ describe('mapFieldMetadataToGraphQLQuery', () => {
fieldMetadata: personObjectMetadataItem.fields.find(
(field) => field.name === 'name',
)!,
objectPermissionsByObjectMetadataId: {},
});
expect(normalizeGQLField(res)).toEqual(
normalizeGQLField(`name
@ -45,6 +47,7 @@ describe('mapFieldMetadataToGraphQLQuery', () => {
fieldMetadata: personObjectMetadataItem.fields.find(
(field) => field.name === 'company',
)!,
objectPermissionsByObjectMetadataId: {},
});
expect(normalizeGQLField(res)).toEqual(
normalizeGQLField(`company
@ -122,6 +125,7 @@ idealCustomerProfile
fieldMetadata: personObjectMetadataItem.fields.find(
(field) => field.name === 'company',
)!,
objectPermissionsByObjectMetadataId: {},
});
expect(normalizeGQLField(res)).toEqual(
normalizeGQLField(`company

View File

@ -30,6 +30,15 @@ describe('mapObjectMetadataToGraphQLQuery', () => {
avatarUrl: true,
companyId: true,
},
objectPermissionsByObjectMetadataId: {
[personObjectMetadataItem.id]: {
canReadObjectRecords: true,
canUpdateObjectRecords: true,
canSoftDeleteObjectRecords: true,
canDestroyObjectRecords: true,
objectMetadataId: personObjectMetadataItem.id,
},
},
});
expect(normalizeGQLQuery(res)).toEqual(
normalizeGQLQuery(`{
@ -124,6 +133,15 @@ describe('mapObjectMetadataToGraphQLQuery', () => {
objectMetadataItems: generatedMockObjectMetadataItems,
objectMetadataItem: personObjectMetadataItem,
recordGqlFields: { company: { id: true }, id: true, name: true },
objectPermissionsByObjectMetadataId: {
[personObjectMetadataItem.id]: {
canReadObjectRecords: true,
canUpdateObjectRecords: true,
canSoftDeleteObjectRecords: true,
canDestroyObjectRecords: true,
objectMetadataId: personObjectMetadataItem.id,
},
},
});
expect(normalizeGQLQuery(res)).toEqual(
normalizeGQLQuery(`{

View File

@ -33,6 +33,7 @@ export const formatFieldMetadataItemAsFieldDefinition = ({
relationObjectMetadataItem?.nameSingular ?? '',
relationObjectMetadataNamePlural:
relationObjectMetadataItem?.namePlural ?? '',
relationObjectMetadataId: relationObjectMetadataItem?.id ?? '',
objectMetadataNameSingular: objectMetadataItem.nameSingular ?? '',
targetFieldMetadataName:
field.relationDefinition?.targetFieldMetadata?.name ?? '',

View File

@ -0,0 +1,28 @@
import { ObjectPermissions } from '@/object-record/cache/types/ObjectPermissions';
import { isDefined } from 'twenty-shared/utils';
import { ObjectPermission } from '~/generated-metadata/graphql';
export const getObjectPermissionsForObject = (
objectPermissionsByObjectMetadataId: Record<string, ObjectPermission>,
objectMetadataId: string,
): ObjectPermissions => {
const objectPermissions =
objectPermissionsByObjectMetadataId[objectMetadataId];
if (!isDefined(objectPermissions)) {
return {
canReadObjectRecords: true,
canUpdateObjectRecords: true,
canSoftDeleteObjectRecords: true,
canDestroyObjectRecords: true,
};
}
return {
canReadObjectRecords: objectPermissions.canReadObjectRecords ?? true,
canUpdateObjectRecords: objectPermissions.canUpdateObjectRecords ?? true,
canSoftDeleteObjectRecords:
objectPermissions.canSoftDeleteObjectRecords ?? true,
canDestroyObjectRecords: objectPermissions.canDestroyObjectRecords ?? true,
};
};

View File

@ -2,12 +2,15 @@ import { mapObjectMetadataToGraphQLQuery } from '@/object-metadata/utils/mapObje
import { isUndefined } from '@sniptt/guards';
import {
FieldMetadataType,
ObjectPermission,
RelationDefinitionType,
} from '~/generated-metadata/graphql';
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
import { getObjectPermissionsForObject } from '@/object-metadata/utils/getObjectPermissionsForObject';
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';
type MapFieldMetadataToGraphQLQueryArgs = {
@ -19,6 +22,7 @@ type MapFieldMetadataToGraphQLQueryArgs = {
>;
relationRecordGqlFields?: RecordGqlFields;
computeReferences?: boolean;
objectPermissionsByObjectMetadataId: Record<string, ObjectPermission>;
};
// TODO: change ObjectMetadataItems mock before refactoring with relationDefinition computed field
export const mapFieldMetadataToGraphQLQuery = ({
@ -27,11 +31,17 @@ export const mapFieldMetadataToGraphQLQuery = ({
fieldMetadata,
relationRecordGqlFields,
computeReferences = false,
objectPermissionsByObjectMetadataId,
}: MapFieldMetadataToGraphQLQueryArgs): string => {
const fieldType = fieldMetadata.type;
const fieldIsNonCompositeField = isNonCompositeField(fieldType);
const objectPermission = getObjectPermissionsForObject(
objectPermissionsByObjectMetadataId,
fieldMetadata.relationDefinition?.targetObjectMetadata.id,
);
if (fieldIsNonCompositeField) {
return gqlField;
}
@ -51,6 +61,15 @@ export const mapFieldMetadataToGraphQLQuery = ({
return '';
}
if (
isDefined(objectPermissionsByObjectMetadataId) &&
isDefined(relationMetadataItem.id)
) {
if (!objectPermission.canReadObjectRecords) {
return '';
}
}
if (gqlField === fieldMetadata.settings?.joinColumnName) {
return `${gqlField}`;
}
@ -62,6 +81,7 @@ ${mapObjectMetadataToGraphQLQuery({
recordGqlFields: relationRecordGqlFields,
computeReferences: computeReferences,
isRootLevel: false,
objectPermissionsByObjectMetadataId,
})}`;
}
@ -80,6 +100,15 @@ ${mapObjectMetadataToGraphQLQuery({
return '';
}
if (
isDefined(objectPermissionsByObjectMetadataId) &&
isDefined(relationMetadataItem.id)
) {
if (!objectPermission.canReadObjectRecords) {
return '';
}
}
return `${gqlField}
{
edges {
@ -89,6 +118,7 @@ ${mapObjectMetadataToGraphQLQuery({
recordGqlFields: relationRecordGqlFields,
computeReferences,
isRootLevel: false,
objectPermissionsByObjectMetadataId,
})}
}
}`;

View File

@ -1,25 +1,47 @@
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 { isDefined } from 'twenty-shared/utils';
import { ObjectPermission } from '~/generated-metadata/graphql';
type MapObjectMetadataToGraphQLQueryArgs = {
objectMetadataItems: ObjectMetadataItem[];
objectMetadataItem: Pick<ObjectMetadataItem, 'nameSingular' | 'fields'>;
objectMetadataItem: Pick<
ObjectMetadataItem,
'nameSingular' | 'fields' | 'id'
>;
recordGqlFields?: RecordGqlFields;
computeReferences?: boolean;
isRootLevel?: boolean;
objectPermissionsByObjectMetadataId: Record<string, ObjectPermission>;
};
export const mapObjectMetadataToGraphQLQuery = ({
objectMetadataItems,
objectMetadataItem,
recordGqlFields,
computeReferences = false,
isRootLevel = true,
objectPermissionsByObjectMetadataId,
}: MapObjectMetadataToGraphQLQueryArgs): string => {
if (
!isRootLevel &&
isDefined(objectPermissionsByObjectMetadataId) &&
isDefined(objectMetadataItem.id)
) {
const objectPermission = getObjectPermissionsForObject(
objectPermissionsByObjectMetadataId,
objectMetadataItem.id,
);
if (!objectPermission.canReadObjectRecords) {
return '';
}
}
const manyToOneRelationFields = objectMetadataItem?.fields
.filter((field) => field.isActive)
.filter((field) => field.type === FieldMetadataType.RELATION)
@ -61,25 +83,29 @@ export const mapObjectMetadataToGraphQLQuery = ({
}`;
}
const mappedFields = gqlFieldWithFieldMetadataThatSouldBeQueried
.map((gqlFieldWithFieldMetadata) => {
const currentRecordGqlFields =
recordGqlFields?.[gqlFieldWithFieldMetadata.gqlField];
const relationRecordGqlFields = isRecordGqlFieldsNode(
currentRecordGqlFields,
)
? currentRecordGqlFields
: undefined;
return mapFieldMetadataToGraphQLQuery({
objectMetadataItems,
gqlField: gqlFieldWithFieldMetadata.gqlField,
fieldMetadata: gqlFieldWithFieldMetadata.fieldMetadata,
relationRecordGqlFields,
computeReferences,
objectPermissionsByObjectMetadataId,
});
})
.filter((field) => field !== '')
.join('\n');
return `{
__typename
${gqlFieldWithFieldMetadataThatSouldBeQueried
.map((gqlFieldWithFieldMetadata) => {
const currentRecordGqlFields =
recordGqlFields?.[gqlFieldWithFieldMetadata.gqlField];
const relationRecordGqlFields = isRecordGqlFieldsNode(
currentRecordGqlFields,
)
? currentRecordGqlFields
: undefined;
return mapFieldMetadataToGraphQLQuery({
objectMetadataItems,
gqlField: gqlFieldWithFieldMetadata.gqlField,
fieldMetadata: gqlFieldWithFieldMetadata.fieldMetadata,
relationRecordGqlFields,
computeReferences,
});
})
.join('\n')}
${mappedFields}
}`;
};