From 8413c6f3dde642a1d70d9b7646ed72dd771bf936 Mon Sep 17 00:00:00 2001 From: Weiko Date: Thu, 15 Jan 2026 17:40:47 +0100 Subject: [PATCH] Fix insert new record with RLS (#17164) ## Context Now that RLS predicates are applied, creating a record through the FE (which is empty by default) is failing if your role has predicates and your input does not respect them (which will always be true since, as said above, input will be pretty much empty) ## Implementation - Moved isMatching* filters to twenty-shared - Implemented isMatchingRlsPredicates utils in the backend (ORM) to check before insertion/update if the record is matching the current user role Rls predicates, reusing the isMatching* filters utils moved to twenty-shared - Frontend now applies RLS predicates before creating a new record (similarly to what we do with view filters) Note: It seems composite were not properly handled with view-filter insertion logic, since I'm reusing the util for now, the issue remains for RLS and will need to be addressed --- .../src/generated-metadata/graphql.ts | 71 +-- .../twenty-front/src/generated/graphql.ts | 2 + .../mapObjectMetadataToGraphQLQuery.test.ts | 6 +- .../utils/getObjectPermissionsForObject.ts | 6 + .../useBuildRecordInputFromRLSPredicates.ts | 121 +++++ .../RecordCalendarMonth.stories.tsx | 2 + .../utils/isRecordMatchingFilter.ts | 33 +- .../__tests__/evaluateTabVisibility.test.ts | 2 + .../hooks/useBuildRecordInputFromFilters.ts | 37 +- .../hooks/useCreateNewIndexRecord.ts | 10 + .../buildValueFromFilter.spec.ts} | 2 +- .../utils/buildRecordInputFromFilter.ts | 359 ++------------- .../utils/buildValueFromFilter.ts | 335 ++++++++++++++ .../__tests__/checkFieldPermissions.test.ts | 4 +- .../__stories__/WidgetRenderer.stories.tsx | 4 + .../fragments/objectPermissionFragment.ts | 10 + ...ectPermissionsFromMapByObjectMetadataId.ts | 2 + .../src/testing/mock-data/users.ts | 6 + .../all-metadata-relations.constant.ts | 6 + ...a-related-metadata-names.util.spec.ts.snap | 2 + ...-role-input-to-flat-role-to-create.util.ts | 2 + .../from-role-entity-to-flat-role.util.ts | 5 + .../dtos/object-permission.dto.ts | 8 + .../field-permissions.service.spec.ts | 4 + .../metadata-modules/role/role.entity.ts | 19 + .../metadata-modules/role/role.module.ts | 6 +- .../workspace-flat-role-map-cache.service.ts | 32 ++ ...rkspace-roles-permissions-cache.service.ts | 22 +- ...ermissionsToUserWorkspacePermissionsDto.ts | 3 + ...-level-permission-predicate-group.input.ts | 2 +- ...te-row-level-permission-predicate.input.ts | 7 +- ...-level-permission-predicate-group.input.ts | 2 +- ...te-row-level-permission-predicate.input.ts | 7 +- ...t-row-level-permission-predicates.input.ts | 8 +- ...ow-level-permission-predicate-group.dto.ts | 2 +- .../row-level-permission-predicate.dto.ts | 7 +- ...level-permission-predicate-group.entity.ts | 9 +- .../row-level-permission-predicate.entity.ts | 11 +- ...vel-permission-predicate-group.resolver.ts | 3 +- ...row-level-permission-predicate.resolver.ts | 3 +- ...evel-permission-predicate-group.service.ts | 15 +- .../row-level-permission-predicate.service.ts | 11 + .../workspace-entity-manager.spec.ts | 2 + .../exceptions/twenty-orm.exception.ts | 6 + .../__tests__/workspace.repository.spec.ts | 2 + .../workspace-insert-query-builder.ts | 23 + .../workspace-update-query-builder.ts | 60 +++ ...mpute-permission-intersection.util.spec.ts | 46 ++ ...row-level-permission-record-filter.util.ts | 4 +- .../compute-permission-intersection.util.ts | 2 + ...rls-row-level-permission-predicate.util.ts | 423 ++++++++++++++++++ ...-orm-graphql-api-exception-handler.util.ts | 1 + ...alidate-rls-predicates-for-records.util.ts | 75 ++++ ...create-standard-role-flat-metadata.util.ts | 2 + .../src/types/ObjectPermissions.ts | 4 + .../src/types/RowLevelPermissionPredicate.ts | 14 + .../types/RowLevelPermissionPredicateGroup.ts | 10 + ...ermissionPredicateGroupLogicalOperator.ts} | 0 .../RowLevelPermissionPredicateOperand.ts} | 0 .../RowLevelPermissionPredicateValue.ts} | 0 packages/twenty-shared/src/types/index.ts | 8 + .../twenty-shared/src/utils/filter/index.ts | 13 + .../__tests__/isMatchingArrayFilter.test.ts | 2 +- .../__tests__/isMatchingBooleanFilter.test.ts | 2 +- .../isMatchingCurrencyFilter.test.ts | 4 +- .../__tests__/isMatchingDateFilter.test.ts | 2 +- .../__tests__/isMatchingFloatFilter.test.ts | 2 +- .../__tests__/isMatchingStringFilter.test.ts | 2 +- .../isMatchingTSVectorFilter.test.ts | 2 +- .../__tests__/isMatchingUUIDFilter.test.ts | 2 +- .../filter}/utils/isMatchingArrayFilter.ts | 2 +- .../filter}/utils/isMatchingBooleanFilter.ts | 2 +- .../filter}/utils/isMatchingCurrencyFilter.ts | 4 +- .../filter}/utils/isMatchingDateFilter.ts | 2 +- .../filter}/utils/isMatchingFloatFilter.ts | 2 +- .../utils/isMatchingMultiSelectFilter.ts | 2 +- .../filter}/utils/isMatchingRatingFilter.ts | 2 +- .../filter}/utils/isMatchingRawJsonFilter.ts | 2 +- .../utils/isMatchingRichTextV2Filter.ts | 2 +- .../filter}/utils/isMatchingSelectFilter.ts | 2 +- .../filter}/utils/isMatchingStringFilter.ts | 2 +- .../filter}/utils/isMatchingTSVectorFilter.ts | 2 +- .../filter}/utils/isMatchingUUIDFilter.ts | 2 +- packages/twenty-shared/src/utils/index.ts | 13 + 84 files changed, 1499 insertions(+), 463 deletions(-) create mode 100644 packages/twenty-front/src/modules/object-record/hooks/useBuildRecordInputFromRLSPredicates.ts rename packages/twenty-front/src/modules/object-record/record-table/utils/{buildRecordInputFromFilter.spec.ts => __tests__/buildValueFromFilter.spec.ts} (99%) create mode 100644 packages/twenty-front/src/modules/object-record/record-table/utils/buildValueFromFilter.ts create mode 100644 packages/twenty-server/src/engine/twenty-orm/utils/is-record-matching-rls-row-level-permission-predicate.util.ts create mode 100644 packages/twenty-server/src/engine/twenty-orm/utils/validate-rls-predicates-for-records.util.ts create mode 100644 packages/twenty-shared/src/types/RowLevelPermissionPredicate.ts create mode 100644 packages/twenty-shared/src/types/RowLevelPermissionPredicateGroup.ts rename packages/{twenty-server/src/engine/metadata-modules/row-level-permission-predicate/enums/row-level-permission-predicate-group-logical-operator.enum.ts => twenty-shared/src/types/RowLevelPermissionPredicateGroupLogicalOperator.ts} (100%) rename packages/{twenty-server/src/engine/metadata-modules/row-level-permission-predicate/enums/row-level-permission-predicate-operand.ts => twenty-shared/src/types/RowLevelPermissionPredicateOperand.ts} (100%) rename packages/{twenty-server/src/engine/metadata-modules/row-level-permission-predicate/types/row-level-permission-predicate-value.type.ts => twenty-shared/src/types/RowLevelPermissionPredicateValue.ts} (100%) rename packages/{twenty-front/src/modules/object-record/record-filter => twenty-shared/src/utils/filter}/utils/__tests__/isMatchingArrayFilter.test.ts (97%) rename packages/{twenty-front/src/modules/object-record/record-filter => twenty-shared/src/utils/filter}/utils/__tests__/isMatchingBooleanFilter.test.ts (90%) rename packages/{twenty-front/src/modules/object-record/record-filter => twenty-shared/src/utils/filter}/utils/__tests__/isMatchingCurrencyFilter.test.ts (98%) rename packages/{twenty-front/src/modules/object-record/record-filter => twenty-shared/src/utils/filter}/utils/__tests__/isMatchingDateFilter.test.ts (97%) rename packages/{twenty-front/src/modules/object-record/record-filter => twenty-shared/src/utils/filter}/utils/__tests__/isMatchingFloatFilter.test.ts (96%) rename packages/{twenty-front/src/modules/object-record/record-filter => twenty-shared/src/utils/filter}/utils/__tests__/isMatchingStringFilter.test.ts (97%) rename packages/{twenty-front/src/modules/object-record/record-filter => twenty-shared/src/utils/filter}/utils/__tests__/isMatchingTSVectorFilter.test.ts (93%) rename packages/{twenty-front/src/modules/object-record/record-filter => twenty-shared/src/utils/filter}/utils/__tests__/isMatchingUUIDFilter.test.ts (94%) rename packages/{twenty-front/src/modules/object-record/record-filter => twenty-shared/src/utils/filter}/utils/isMatchingArrayFilter.ts (93%) rename packages/{twenty-front/src/modules/object-record/record-filter => twenty-shared/src/utils/filter}/utils/isMatchingBooleanFilter.ts (89%) rename packages/{twenty-front/src/modules/object-record/record-filter => twenty-shared/src/utils/filter}/utils/isMatchingCurrencyFilter.ts (96%) rename packages/{twenty-front/src/modules/object-record/record-filter => twenty-shared/src/utils/filter}/utils/isMatchingDateFilter.ts (96%) rename packages/{twenty-front/src/modules/object-record/record-filter => twenty-shared/src/utils/filter}/utils/isMatchingFloatFilter.ts (94%) rename packages/{twenty-front/src/modules/object-record/record-filter => twenty-shared/src/utils/filter}/utils/isMatchingMultiSelectFilter.ts (93%) rename packages/{twenty-front/src/modules/object-record/record-filter => twenty-shared/src/utils/filter}/utils/isMatchingRatingFilter.ts (92%) rename packages/{twenty-front/src/modules/object-record/record-filter => twenty-shared/src/utils/filter}/utils/isMatchingRawJsonFilter.ts (92%) rename packages/{twenty-front/src/modules/object-record/record-filter => twenty-shared/src/utils/filter}/utils/isMatchingRichTextV2Filter.ts (91%) rename packages/{twenty-front/src/modules/object-record/record-filter => twenty-shared/src/utils/filter}/utils/isMatchingSelectFilter.ts (92%) rename packages/{twenty-front/src/modules/object-record/record-filter => twenty-shared/src/utils/filter}/utils/isMatchingStringFilter.ts (97%) rename packages/{twenty-front/src/modules/object-record/record-filter => twenty-shared/src/utils/filter}/utils/isMatchingTSVectorFilter.ts (92%) rename packages/{twenty-front/src/modules/object-record/record-filter => twenty-shared/src/utils/filter}/utils/isMatchingUUIDFilter.ts (90%) diff --git a/packages/twenty-front/src/generated-metadata/graphql.ts b/packages/twenty-front/src/generated-metadata/graphql.ts index 3efd63533a..35e0f509e2 100644 --- a/packages/twenty-front/src/generated-metadata/graphql.ts +++ b/packages/twenty-front/src/generated-metadata/graphql.ts @@ -3089,6 +3089,8 @@ export type ObjectPermission = { canUpdateObjectRecords?: Maybe; objectMetadataId: Scalars['UUID']; restrictedFields?: Maybe; + rowLevelPermissionPredicateGroups?: Maybe>; + rowLevelPermissionPredicates?: Maybe>; }; export type ObjectPermissionInput = { @@ -6113,7 +6115,7 @@ export type ApiKeyForRoleFragmentFragment = { __typename?: 'ApiKeyForRole', id: export type FieldPermissionFragmentFragment = { __typename?: 'FieldPermission', objectMetadataId: string, fieldMetadataId: string, canReadFieldValue?: boolean | null, canUpdateFieldValue?: boolean | null, id: string, roleId: string }; -export type ObjectPermissionFragmentFragment = { __typename?: 'ObjectPermission', objectMetadataId: string, canReadObjectRecords?: boolean | null, canUpdateObjectRecords?: boolean | null, canSoftDeleteObjectRecords?: boolean | null, canDestroyObjectRecords?: boolean | null, restrictedFields?: any | null }; +export type ObjectPermissionFragmentFragment = { __typename?: 'ObjectPermission', objectMetadataId: string, canReadObjectRecords?: boolean | null, canUpdateObjectRecords?: boolean | null, canSoftDeleteObjectRecords?: boolean | null, canDestroyObjectRecords?: boolean | null, restrictedFields?: any | null, rowLevelPermissionPredicates?: Array<{ __typename?: 'RowLevelPermissionPredicate', id: string, fieldMetadataId: string, objectMetadataId: string, operand: RowLevelPermissionPredicateOperand, subFieldName?: string | null, workspaceMemberFieldMetadataId?: string | null, workspaceMemberSubFieldName?: string | null, rowLevelPermissionPredicateGroupId?: string | null, positionInRowLevelPermissionPredicateGroup?: number | null, roleId: string, value?: any | null }> | null, rowLevelPermissionPredicateGroups?: Array<{ __typename?: 'RowLevelPermissionPredicateGroup', id: string, parentRowLevelPermissionPredicateGroupId?: string | null, logicalOperator: RowLevelPermissionPredicateGroupLogicalOperator, positionInRowLevelPermissionPredicateGroup?: number | null, roleId: string, objectMetadataId: string }> | null }; export type PermissionFlagFragmentFragment = { __typename?: 'PermissionFlag', id: string, flag: PermissionFlagType, roleId: string }; @@ -6164,7 +6166,7 @@ export type UpsertObjectPermissionsMutationVariables = Exact<{ }>; -export type UpsertObjectPermissionsMutation = { __typename?: 'Mutation', upsertObjectPermissions: Array<{ __typename?: 'ObjectPermission', objectMetadataId: string, canReadObjectRecords?: boolean | null, canUpdateObjectRecords?: boolean | null, canSoftDeleteObjectRecords?: boolean | null, canDestroyObjectRecords?: boolean | null, restrictedFields?: any | null }> }; +export type UpsertObjectPermissionsMutation = { __typename?: 'Mutation', upsertObjectPermissions: Array<{ __typename?: 'ObjectPermission', objectMetadataId: string, canReadObjectRecords?: boolean | null, canUpdateObjectRecords?: boolean | null, canSoftDeleteObjectRecords?: boolean | null, canDestroyObjectRecords?: boolean | null, restrictedFields?: any | null, rowLevelPermissionPredicates?: Array<{ __typename?: 'RowLevelPermissionPredicate', id: string, fieldMetadataId: string, objectMetadataId: string, operand: RowLevelPermissionPredicateOperand, subFieldName?: string | null, workspaceMemberFieldMetadataId?: string | null, workspaceMemberSubFieldName?: string | null, rowLevelPermissionPredicateGroupId?: string | null, positionInRowLevelPermissionPredicateGroup?: number | null, roleId: string, value?: any | null }> | null, rowLevelPermissionPredicateGroups?: Array<{ __typename?: 'RowLevelPermissionPredicateGroup', id: string, parentRowLevelPermissionPredicateGroupId?: string | null, logicalOperator: RowLevelPermissionPredicateGroupLogicalOperator, positionInRowLevelPermissionPredicateGroup?: number | null, roleId: string, objectMetadataId: string }> | null }> }; export type UpsertPermissionFlagsMutationVariables = Exact<{ upsertPermissionFlagsInput: UpsertPermissionFlagsInput; @@ -6183,7 +6185,7 @@ export type UpsertRowLevelPermissionPredicatesMutation = { __typename?: 'Mutatio export type GetRolesQueryVariables = Exact<{ [key: string]: never; }>; -export type GetRolesQuery = { __typename?: 'Query', getRoles: Array<{ __typename?: 'Role', id: string, label: string, description?: string | null, icon?: string | null, canUpdateAllSettings: boolean, canAccessAllTools: boolean, isEditable: boolean, canReadAllObjectRecords: boolean, canUpdateAllObjectRecords: boolean, canSoftDeleteAllObjectRecords: boolean, canDestroyAllObjectRecords: boolean, canBeAssignedToUsers: boolean, canBeAssignedToAgents: boolean, canBeAssignedToApiKeys: boolean, workspaceMembers: Array<{ __typename?: 'WorkspaceMember', id: string, avatarUrl?: string | null, userEmail: string, name: { __typename?: 'FullName', firstName: string, lastName: string } }>, agents: Array<{ __typename?: 'Agent', id: string, name: string, label: string, description?: string | null, icon?: string | null, prompt: string, modelId: string, responseFormat?: any | null, roleId?: string | null, isCustom: boolean, modelConfiguration?: any | null, evaluationInputs: Array, applicationId?: string | null, createdAt: string, updatedAt: string }>, apiKeys: Array<{ __typename?: 'ApiKeyForRole', id: string, name: string, expiresAt: string, revokedAt?: string | null }>, permissionFlags?: Array<{ __typename?: 'PermissionFlag', id: string, flag: PermissionFlagType, roleId: string }> | null, objectPermissions?: Array<{ __typename?: 'ObjectPermission', objectMetadataId: string, canReadObjectRecords?: boolean | null, canUpdateObjectRecords?: boolean | null, canSoftDeleteObjectRecords?: boolean | null, canDestroyObjectRecords?: boolean | null, restrictedFields?: any | null }> | null, fieldPermissions?: Array<{ __typename?: 'FieldPermission', objectMetadataId: string, fieldMetadataId: string, canReadFieldValue?: boolean | null, canUpdateFieldValue?: boolean | null, id: string, roleId: string }> | null, rowLevelPermissionPredicates?: Array<{ __typename?: 'RowLevelPermissionPredicate', id: string, fieldMetadataId: string, objectMetadataId: string, operand: RowLevelPermissionPredicateOperand, subFieldName?: string | null, workspaceMemberFieldMetadataId?: string | null, workspaceMemberSubFieldName?: string | null, rowLevelPermissionPredicateGroupId?: string | null, positionInRowLevelPermissionPredicateGroup?: number | null, roleId: string, value?: any | null }> | null, rowLevelPermissionPredicateGroups?: Array<{ __typename?: 'RowLevelPermissionPredicateGroup', id: string, parentRowLevelPermissionPredicateGroupId?: string | null, logicalOperator: RowLevelPermissionPredicateGroupLogicalOperator, positionInRowLevelPermissionPredicateGroup?: number | null, roleId: string, objectMetadataId: string }> | null }> }; +export type GetRolesQuery = { __typename?: 'Query', getRoles: Array<{ __typename?: 'Role', id: string, label: string, description?: string | null, icon?: string | null, canUpdateAllSettings: boolean, canAccessAllTools: boolean, isEditable: boolean, canReadAllObjectRecords: boolean, canUpdateAllObjectRecords: boolean, canSoftDeleteAllObjectRecords: boolean, canDestroyAllObjectRecords: boolean, canBeAssignedToUsers: boolean, canBeAssignedToAgents: boolean, canBeAssignedToApiKeys: boolean, workspaceMembers: Array<{ __typename?: 'WorkspaceMember', id: string, avatarUrl?: string | null, userEmail: string, name: { __typename?: 'FullName', firstName: string, lastName: string } }>, agents: Array<{ __typename?: 'Agent', id: string, name: string, label: string, description?: string | null, icon?: string | null, prompt: string, modelId: string, responseFormat?: any | null, roleId?: string | null, isCustom: boolean, modelConfiguration?: any | null, evaluationInputs: Array, applicationId?: string | null, createdAt: string, updatedAt: string }>, apiKeys: Array<{ __typename?: 'ApiKeyForRole', id: string, name: string, expiresAt: string, revokedAt?: string | null }>, permissionFlags?: Array<{ __typename?: 'PermissionFlag', id: string, flag: PermissionFlagType, roleId: string }> | null, objectPermissions?: Array<{ __typename?: 'ObjectPermission', objectMetadataId: string, canReadObjectRecords?: boolean | null, canUpdateObjectRecords?: boolean | null, canSoftDeleteObjectRecords?: boolean | null, canDestroyObjectRecords?: boolean | null, restrictedFields?: any | null, rowLevelPermissionPredicates?: Array<{ __typename?: 'RowLevelPermissionPredicate', id: string, fieldMetadataId: string, objectMetadataId: string, operand: RowLevelPermissionPredicateOperand, subFieldName?: string | null, workspaceMemberFieldMetadataId?: string | null, workspaceMemberSubFieldName?: string | null, rowLevelPermissionPredicateGroupId?: string | null, positionInRowLevelPermissionPredicateGroup?: number | null, roleId: string, value?: any | null }> | null, rowLevelPermissionPredicateGroups?: Array<{ __typename?: 'RowLevelPermissionPredicateGroup', id: string, parentRowLevelPermissionPredicateGroupId?: string | null, logicalOperator: RowLevelPermissionPredicateGroupLogicalOperator, positionInRowLevelPermissionPredicateGroup?: number | null, roleId: string, objectMetadataId: string }> | null }> | null, fieldPermissions?: Array<{ __typename?: 'FieldPermission', objectMetadataId: string, fieldMetadataId: string, canReadFieldValue?: boolean | null, canUpdateFieldValue?: boolean | null, id: string, roleId: string }> | null, rowLevelPermissionPredicates?: Array<{ __typename?: 'RowLevelPermissionPredicate', id: string, fieldMetadataId: string, objectMetadataId: string, operand: RowLevelPermissionPredicateOperand, subFieldName?: string | null, workspaceMemberFieldMetadataId?: string | null, workspaceMemberSubFieldName?: string | null, rowLevelPermissionPredicateGroupId?: string | null, positionInRowLevelPermissionPredicateGroup?: number | null, roleId: string, value?: any | null }> | null, rowLevelPermissionPredicateGroups?: Array<{ __typename?: 'RowLevelPermissionPredicateGroup', id: string, parentRowLevelPermissionPredicateGroupId?: string | null, logicalOperator: RowLevelPermissionPredicateGroupLogicalOperator, positionInRowLevelPermissionPredicateGroup?: number | null, roleId: string, objectMetadataId: string }> | null }> }; export type CreateApprovedAccessDomainMutationVariables = Exact<{ input: CreateApprovedAccessDomainInput; @@ -6318,7 +6320,7 @@ export type BillingSubscriptionFragmentFragment = { __typename?: 'BillingSubscri export type CurrentBillingSubscriptionFragmentFragment = { __typename?: 'BillingSubscription', id: string, status: SubscriptionStatus, interval?: SubscriptionInterval | null, metadata: any, currentPeriodEnd?: string | null, phases: Array<{ __typename?: 'BillingSubscriptionSchedulePhase', start_date: number, end_date: number, items: Array<{ __typename?: 'BillingSubscriptionSchedulePhaseItem', price: string, quantity?: number | null }> }>, billingSubscriptionItems?: Array<{ __typename?: 'BillingSubscriptionItemDTO', id: string, hasReachedCurrentPeriodCap: boolean, quantity?: number | null, stripePriceId: string, billingProduct: { __typename?: 'BillingLicensedProduct', name: string, description: string, images?: Array | null, metadata: { __typename?: 'BillingProductMetadata', productKey: BillingProductKey, planKey: BillingPlanKey, priceUsageBased: BillingUsageType } } | { __typename?: 'BillingMeteredProduct', name: string, description: string, images?: Array | null, metadata: { __typename?: 'BillingProductMetadata', productKey: BillingProductKey, planKey: BillingPlanKey, priceUsageBased: BillingUsageType } } }> | null }; -export type UserQueryFragmentFragment = { __typename?: 'User', id: string, firstName: string, lastName: string, email: string, hasPassword: boolean, canAccessFullAdminPanel: boolean, canImpersonate: boolean, supportUserHash?: string | null, onboardingStatus?: OnboardingStatus | null, userVars?: any | null, workspaceMember?: { __typename?: 'WorkspaceMember', id: string, colorScheme: string, avatarUrl?: string | null, locale?: string | null, userEmail: string, timeZone?: string | null, dateFormat?: WorkspaceMemberDateFormatEnum | null, timeFormat?: WorkspaceMemberTimeFormatEnum | null, calendarStartDay?: number | null, numberFormat?: WorkspaceMemberNumberFormatEnum | null, name: { __typename?: 'FullName', firstName: string, lastName: string } } | null, workspaceMembers?: Array<{ __typename?: 'WorkspaceMember', id: string, avatarUrl?: string | null, userEmail: string, name: { __typename?: 'FullName', firstName: string, lastName: string } }> | null, deletedWorkspaceMembers?: Array<{ __typename?: 'DeletedWorkspaceMember', id: string, avatarUrl?: string | null, userEmail: string, name: { __typename?: 'FullName', firstName: string, lastName: string } }> | null, currentUserWorkspace?: { __typename?: 'UserWorkspace', id: string, permissionFlags?: Array | null, objectsPermissions?: Array<{ __typename?: 'ObjectPermission', objectMetadataId: string, canReadObjectRecords?: boolean | null, canUpdateObjectRecords?: boolean | null, canSoftDeleteObjectRecords?: boolean | null, canDestroyObjectRecords?: boolean | null, restrictedFields?: any | null }> | null, twoFactorAuthenticationMethodSummary?: Array<{ __typename?: 'TwoFactorAuthenticationMethodDTO', twoFactorAuthenticationMethodId: string, status: string, strategy: string }> | null } | null, currentWorkspace?: { __typename?: 'Workspace', id: string, displayName?: string | null, logo?: string | null, inviteHash?: string | null, allowImpersonation: boolean, activationStatus: WorkspaceActivationStatus, isPublicInviteLinkEnabled: boolean, isGoogleAuthEnabled: boolean, isMicrosoftAuthEnabled: boolean, isPasswordAuthEnabled: boolean, isGoogleAuthBypassEnabled: boolean, isMicrosoftAuthBypassEnabled: boolean, isPasswordAuthBypassEnabled: boolean, subdomain: string, hasValidEnterpriseKey: boolean, isCustomDomainEnabled: boolean, metadataVersion: number, workspaceMembersCount?: number | null, fastModel: string, smartModel: string, isTwoFactorAuthenticationEnforced: boolean, trashRetentionDays: number, editableProfileFields?: Array | null, workspaceCustomApplication?: { __typename?: 'Application', id: string } | null, workspaceUrls: { __typename?: 'WorkspaceUrls', subdomainUrl: string, customUrl?: string | null }, featureFlags?: Array<{ __typename?: 'FeatureFlagDTO', key: FeatureFlagKey, value: boolean }> | null, currentBillingSubscription?: { __typename?: 'BillingSubscription', id: string, status: SubscriptionStatus, interval?: SubscriptionInterval | null, metadata: any, currentPeriodEnd?: string | null, phases: Array<{ __typename?: 'BillingSubscriptionSchedulePhase', start_date: number, end_date: number, items: Array<{ __typename?: 'BillingSubscriptionSchedulePhaseItem', price: string, quantity?: number | null }> }>, billingSubscriptionItems?: Array<{ __typename?: 'BillingSubscriptionItemDTO', id: string, hasReachedCurrentPeriodCap: boolean, quantity?: number | null, stripePriceId: string, billingProduct: { __typename?: 'BillingLicensedProduct', name: string, description: string, images?: Array | null, metadata: { __typename?: 'BillingProductMetadata', productKey: BillingProductKey, planKey: BillingPlanKey, priceUsageBased: BillingUsageType } } | { __typename?: 'BillingMeteredProduct', name: string, description: string, images?: Array | null, metadata: { __typename?: 'BillingProductMetadata', productKey: BillingProductKey, planKey: BillingPlanKey, priceUsageBased: BillingUsageType } } }> | null } | null, billingSubscriptions: Array<{ __typename?: 'BillingSubscription', id: string, status: SubscriptionStatus, metadata: any, phases: Array<{ __typename?: 'BillingSubscriptionSchedulePhase', start_date: number, end_date: number, items: Array<{ __typename?: 'BillingSubscriptionSchedulePhaseItem', price: string, quantity?: number | null }> }> }>, defaultRole?: { __typename?: 'Role', id: string, label: string, description?: string | null, icon?: string | null, canUpdateAllSettings: boolean, canAccessAllTools: boolean, isEditable: boolean, canReadAllObjectRecords: boolean, canUpdateAllObjectRecords: boolean, canSoftDeleteAllObjectRecords: boolean, canDestroyAllObjectRecords: boolean, canBeAssignedToUsers: boolean, canBeAssignedToAgents: boolean, canBeAssignedToApiKeys: boolean } | null } | null, availableWorkspaces: { __typename?: 'AvailableWorkspaces', availableWorkspacesForSignIn: Array<{ __typename?: 'AvailableWorkspace', id: string, displayName?: string | null, loginToken?: string | null, inviteHash?: string | null, personalInviteToken?: string | null, logo?: string | null, workspaceUrls: { __typename?: 'WorkspaceUrls', subdomainUrl: string, customUrl?: string | null }, sso: Array<{ __typename?: 'SSOConnection', type: IdentityProviderType, id: string, issuer: string, name: string, status: SsoIdentityProviderStatus }> }>, availableWorkspacesForSignUp: Array<{ __typename?: 'AvailableWorkspace', id: string, displayName?: string | null, loginToken?: string | null, inviteHash?: string | null, personalInviteToken?: string | null, logo?: string | null, workspaceUrls: { __typename?: 'WorkspaceUrls', subdomainUrl: string, customUrl?: string | null }, sso: Array<{ __typename?: 'SSOConnection', type: IdentityProviderType, id: string, issuer: string, name: string, status: SsoIdentityProviderStatus }> }> } }; +export type UserQueryFragmentFragment = { __typename?: 'User', id: string, firstName: string, lastName: string, email: string, hasPassword: boolean, canAccessFullAdminPanel: boolean, canImpersonate: boolean, supportUserHash?: string | null, onboardingStatus?: OnboardingStatus | null, userVars?: any | null, workspaceMember?: { __typename?: 'WorkspaceMember', id: string, colorScheme: string, avatarUrl?: string | null, locale?: string | null, userEmail: string, timeZone?: string | null, dateFormat?: WorkspaceMemberDateFormatEnum | null, timeFormat?: WorkspaceMemberTimeFormatEnum | null, calendarStartDay?: number | null, numberFormat?: WorkspaceMemberNumberFormatEnum | null, name: { __typename?: 'FullName', firstName: string, lastName: string } } | null, workspaceMembers?: Array<{ __typename?: 'WorkspaceMember', id: string, avatarUrl?: string | null, userEmail: string, name: { __typename?: 'FullName', firstName: string, lastName: string } }> | null, deletedWorkspaceMembers?: Array<{ __typename?: 'DeletedWorkspaceMember', id: string, avatarUrl?: string | null, userEmail: string, name: { __typename?: 'FullName', firstName: string, lastName: string } }> | null, currentUserWorkspace?: { __typename?: 'UserWorkspace', id: string, permissionFlags?: Array | null, objectsPermissions?: Array<{ __typename?: 'ObjectPermission', objectMetadataId: string, canReadObjectRecords?: boolean | null, canUpdateObjectRecords?: boolean | null, canSoftDeleteObjectRecords?: boolean | null, canDestroyObjectRecords?: boolean | null, restrictedFields?: any | null, rowLevelPermissionPredicates?: Array<{ __typename?: 'RowLevelPermissionPredicate', id: string, fieldMetadataId: string, objectMetadataId: string, operand: RowLevelPermissionPredicateOperand, subFieldName?: string | null, workspaceMemberFieldMetadataId?: string | null, workspaceMemberSubFieldName?: string | null, rowLevelPermissionPredicateGroupId?: string | null, positionInRowLevelPermissionPredicateGroup?: number | null, roleId: string, value?: any | null }> | null, rowLevelPermissionPredicateGroups?: Array<{ __typename?: 'RowLevelPermissionPredicateGroup', id: string, parentRowLevelPermissionPredicateGroupId?: string | null, logicalOperator: RowLevelPermissionPredicateGroupLogicalOperator, positionInRowLevelPermissionPredicateGroup?: number | null, roleId: string, objectMetadataId: string }> | null }> | null, twoFactorAuthenticationMethodSummary?: Array<{ __typename?: 'TwoFactorAuthenticationMethodDTO', twoFactorAuthenticationMethodId: string, status: string, strategy: string }> | null } | null, currentWorkspace?: { __typename?: 'Workspace', id: string, displayName?: string | null, logo?: string | null, inviteHash?: string | null, allowImpersonation: boolean, activationStatus: WorkspaceActivationStatus, isPublicInviteLinkEnabled: boolean, isGoogleAuthEnabled: boolean, isMicrosoftAuthEnabled: boolean, isPasswordAuthEnabled: boolean, isGoogleAuthBypassEnabled: boolean, isMicrosoftAuthBypassEnabled: boolean, isPasswordAuthBypassEnabled: boolean, subdomain: string, hasValidEnterpriseKey: boolean, isCustomDomainEnabled: boolean, metadataVersion: number, workspaceMembersCount?: number | null, fastModel: string, smartModel: string, isTwoFactorAuthenticationEnforced: boolean, trashRetentionDays: number, editableProfileFields?: Array | null, workspaceCustomApplication?: { __typename?: 'Application', id: string } | null, workspaceUrls: { __typename?: 'WorkspaceUrls', subdomainUrl: string, customUrl?: string | null }, featureFlags?: Array<{ __typename?: 'FeatureFlagDTO', key: FeatureFlagKey, value: boolean }> | null, currentBillingSubscription?: { __typename?: 'BillingSubscription', id: string, status: SubscriptionStatus, interval?: SubscriptionInterval | null, metadata: any, currentPeriodEnd?: string | null, phases: Array<{ __typename?: 'BillingSubscriptionSchedulePhase', start_date: number, end_date: number, items: Array<{ __typename?: 'BillingSubscriptionSchedulePhaseItem', price: string, quantity?: number | null }> }>, billingSubscriptionItems?: Array<{ __typename?: 'BillingSubscriptionItemDTO', id: string, hasReachedCurrentPeriodCap: boolean, quantity?: number | null, stripePriceId: string, billingProduct: { __typename?: 'BillingLicensedProduct', name: string, description: string, images?: Array | null, metadata: { __typename?: 'BillingProductMetadata', productKey: BillingProductKey, planKey: BillingPlanKey, priceUsageBased: BillingUsageType } } | { __typename?: 'BillingMeteredProduct', name: string, description: string, images?: Array | null, metadata: { __typename?: 'BillingProductMetadata', productKey: BillingProductKey, planKey: BillingPlanKey, priceUsageBased: BillingUsageType } } }> | null } | null, billingSubscriptions: Array<{ __typename?: 'BillingSubscription', id: string, status: SubscriptionStatus, metadata: any, phases: Array<{ __typename?: 'BillingSubscriptionSchedulePhase', start_date: number, end_date: number, items: Array<{ __typename?: 'BillingSubscriptionSchedulePhaseItem', price: string, quantity?: number | null }> }> }>, defaultRole?: { __typename?: 'Role', id: string, label: string, description?: string | null, icon?: string | null, canUpdateAllSettings: boolean, canAccessAllTools: boolean, isEditable: boolean, canReadAllObjectRecords: boolean, canUpdateAllObjectRecords: boolean, canSoftDeleteAllObjectRecords: boolean, canDestroyAllObjectRecords: boolean, canBeAssignedToUsers: boolean, canBeAssignedToAgents: boolean, canBeAssignedToApiKeys: boolean } | null } | null, availableWorkspaces: { __typename?: 'AvailableWorkspaces', availableWorkspacesForSignIn: Array<{ __typename?: 'AvailableWorkspace', id: string, displayName?: string | null, loginToken?: string | null, inviteHash?: string | null, personalInviteToken?: string | null, logo?: string | null, workspaceUrls: { __typename?: 'WorkspaceUrls', subdomainUrl: string, customUrl?: string | null }, sso: Array<{ __typename?: 'SSOConnection', type: IdentityProviderType, id: string, issuer: string, name: string, status: SsoIdentityProviderStatus }> }>, availableWorkspacesForSignUp: Array<{ __typename?: 'AvailableWorkspace', id: string, displayName?: string | null, loginToken?: string | null, inviteHash?: string | null, personalInviteToken?: string | null, logo?: string | null, workspaceUrls: { __typename?: 'WorkspaceUrls', subdomainUrl: string, customUrl?: string | null }, sso: Array<{ __typename?: 'SSOConnection', type: IdentityProviderType, id: string, issuer: string, name: string, status: SsoIdentityProviderStatus }> }> } }; export type WorkspaceUrlsFragmentFragment = { __typename?: 'WorkspaceUrls', subdomainUrl: string, customUrl?: string | null }; @@ -6337,7 +6339,7 @@ export type DeleteUserWorkspaceMutation = { __typename?: 'Mutation', deleteUserF export type GetCurrentUserQueryVariables = Exact<{ [key: string]: never; }>; -export type GetCurrentUserQuery = { __typename?: 'Query', currentUser: { __typename?: 'User', id: string, firstName: string, lastName: string, email: string, hasPassword: boolean, canAccessFullAdminPanel: boolean, canImpersonate: boolean, supportUserHash?: string | null, onboardingStatus?: OnboardingStatus | null, userVars?: any | null, workspaceMember?: { __typename?: 'WorkspaceMember', id: string, colorScheme: string, avatarUrl?: string | null, locale?: string | null, userEmail: string, timeZone?: string | null, dateFormat?: WorkspaceMemberDateFormatEnum | null, timeFormat?: WorkspaceMemberTimeFormatEnum | null, calendarStartDay?: number | null, numberFormat?: WorkspaceMemberNumberFormatEnum | null, name: { __typename?: 'FullName', firstName: string, lastName: string } } | null, workspaceMembers?: Array<{ __typename?: 'WorkspaceMember', id: string, avatarUrl?: string | null, userEmail: string, name: { __typename?: 'FullName', firstName: string, lastName: string } }> | null, deletedWorkspaceMembers?: Array<{ __typename?: 'DeletedWorkspaceMember', id: string, avatarUrl?: string | null, userEmail: string, name: { __typename?: 'FullName', firstName: string, lastName: string } }> | null, currentUserWorkspace?: { __typename?: 'UserWorkspace', id: string, permissionFlags?: Array | null, objectsPermissions?: Array<{ __typename?: 'ObjectPermission', objectMetadataId: string, canReadObjectRecords?: boolean | null, canUpdateObjectRecords?: boolean | null, canSoftDeleteObjectRecords?: boolean | null, canDestroyObjectRecords?: boolean | null, restrictedFields?: any | null }> | null, twoFactorAuthenticationMethodSummary?: Array<{ __typename?: 'TwoFactorAuthenticationMethodDTO', twoFactorAuthenticationMethodId: string, status: string, strategy: string }> | null } | null, currentWorkspace?: { __typename?: 'Workspace', id: string, displayName?: string | null, logo?: string | null, inviteHash?: string | null, allowImpersonation: boolean, activationStatus: WorkspaceActivationStatus, isPublicInviteLinkEnabled: boolean, isGoogleAuthEnabled: boolean, isMicrosoftAuthEnabled: boolean, isPasswordAuthEnabled: boolean, isGoogleAuthBypassEnabled: boolean, isMicrosoftAuthBypassEnabled: boolean, isPasswordAuthBypassEnabled: boolean, subdomain: string, hasValidEnterpriseKey: boolean, isCustomDomainEnabled: boolean, metadataVersion: number, workspaceMembersCount?: number | null, fastModel: string, smartModel: string, isTwoFactorAuthenticationEnforced: boolean, trashRetentionDays: number, editableProfileFields?: Array | null, workspaceCustomApplication?: { __typename?: 'Application', id: string } | null, workspaceUrls: { __typename?: 'WorkspaceUrls', subdomainUrl: string, customUrl?: string | null }, featureFlags?: Array<{ __typename?: 'FeatureFlagDTO', key: FeatureFlagKey, value: boolean }> | null, currentBillingSubscription?: { __typename?: 'BillingSubscription', id: string, status: SubscriptionStatus, interval?: SubscriptionInterval | null, metadata: any, currentPeriodEnd?: string | null, phases: Array<{ __typename?: 'BillingSubscriptionSchedulePhase', start_date: number, end_date: number, items: Array<{ __typename?: 'BillingSubscriptionSchedulePhaseItem', price: string, quantity?: number | null }> }>, billingSubscriptionItems?: Array<{ __typename?: 'BillingSubscriptionItemDTO', id: string, hasReachedCurrentPeriodCap: boolean, quantity?: number | null, stripePriceId: string, billingProduct: { __typename?: 'BillingLicensedProduct', name: string, description: string, images?: Array | null, metadata: { __typename?: 'BillingProductMetadata', productKey: BillingProductKey, planKey: BillingPlanKey, priceUsageBased: BillingUsageType } } | { __typename?: 'BillingMeteredProduct', name: string, description: string, images?: Array | null, metadata: { __typename?: 'BillingProductMetadata', productKey: BillingProductKey, planKey: BillingPlanKey, priceUsageBased: BillingUsageType } } }> | null } | null, billingSubscriptions: Array<{ __typename?: 'BillingSubscription', id: string, status: SubscriptionStatus, metadata: any, phases: Array<{ __typename?: 'BillingSubscriptionSchedulePhase', start_date: number, end_date: number, items: Array<{ __typename?: 'BillingSubscriptionSchedulePhaseItem', price: string, quantity?: number | null }> }> }>, defaultRole?: { __typename?: 'Role', id: string, label: string, description?: string | null, icon?: string | null, canUpdateAllSettings: boolean, canAccessAllTools: boolean, isEditable: boolean, canReadAllObjectRecords: boolean, canUpdateAllObjectRecords: boolean, canSoftDeleteAllObjectRecords: boolean, canDestroyAllObjectRecords: boolean, canBeAssignedToUsers: boolean, canBeAssignedToAgents: boolean, canBeAssignedToApiKeys: boolean } | null } | null, availableWorkspaces: { __typename?: 'AvailableWorkspaces', availableWorkspacesForSignIn: Array<{ __typename?: 'AvailableWorkspace', id: string, displayName?: string | null, loginToken?: string | null, inviteHash?: string | null, personalInviteToken?: string | null, logo?: string | null, workspaceUrls: { __typename?: 'WorkspaceUrls', subdomainUrl: string, customUrl?: string | null }, sso: Array<{ __typename?: 'SSOConnection', type: IdentityProviderType, id: string, issuer: string, name: string, status: SsoIdentityProviderStatus }> }>, availableWorkspacesForSignUp: Array<{ __typename?: 'AvailableWorkspace', id: string, displayName?: string | null, loginToken?: string | null, inviteHash?: string | null, personalInviteToken?: string | null, logo?: string | null, workspaceUrls: { __typename?: 'WorkspaceUrls', subdomainUrl: string, customUrl?: string | null }, sso: Array<{ __typename?: 'SSOConnection', type: IdentityProviderType, id: string, issuer: string, name: string, status: SsoIdentityProviderStatus }> }> } } }; +export type GetCurrentUserQuery = { __typename?: 'Query', currentUser: { __typename?: 'User', id: string, firstName: string, lastName: string, email: string, hasPassword: boolean, canAccessFullAdminPanel: boolean, canImpersonate: boolean, supportUserHash?: string | null, onboardingStatus?: OnboardingStatus | null, userVars?: any | null, workspaceMember?: { __typename?: 'WorkspaceMember', id: string, colorScheme: string, avatarUrl?: string | null, locale?: string | null, userEmail: string, timeZone?: string | null, dateFormat?: WorkspaceMemberDateFormatEnum | null, timeFormat?: WorkspaceMemberTimeFormatEnum | null, calendarStartDay?: number | null, numberFormat?: WorkspaceMemberNumberFormatEnum | null, name: { __typename?: 'FullName', firstName: string, lastName: string } } | null, workspaceMembers?: Array<{ __typename?: 'WorkspaceMember', id: string, avatarUrl?: string | null, userEmail: string, name: { __typename?: 'FullName', firstName: string, lastName: string } }> | null, deletedWorkspaceMembers?: Array<{ __typename?: 'DeletedWorkspaceMember', id: string, avatarUrl?: string | null, userEmail: string, name: { __typename?: 'FullName', firstName: string, lastName: string } }> | null, currentUserWorkspace?: { __typename?: 'UserWorkspace', id: string, permissionFlags?: Array | null, objectsPermissions?: Array<{ __typename?: 'ObjectPermission', objectMetadataId: string, canReadObjectRecords?: boolean | null, canUpdateObjectRecords?: boolean | null, canSoftDeleteObjectRecords?: boolean | null, canDestroyObjectRecords?: boolean | null, restrictedFields?: any | null, rowLevelPermissionPredicates?: Array<{ __typename?: 'RowLevelPermissionPredicate', id: string, fieldMetadataId: string, objectMetadataId: string, operand: RowLevelPermissionPredicateOperand, subFieldName?: string | null, workspaceMemberFieldMetadataId?: string | null, workspaceMemberSubFieldName?: string | null, rowLevelPermissionPredicateGroupId?: string | null, positionInRowLevelPermissionPredicateGroup?: number | null, roleId: string, value?: any | null }> | null, rowLevelPermissionPredicateGroups?: Array<{ __typename?: 'RowLevelPermissionPredicateGroup', id: string, parentRowLevelPermissionPredicateGroupId?: string | null, logicalOperator: RowLevelPermissionPredicateGroupLogicalOperator, positionInRowLevelPermissionPredicateGroup?: number | null, roleId: string, objectMetadataId: string }> | null }> | null, twoFactorAuthenticationMethodSummary?: Array<{ __typename?: 'TwoFactorAuthenticationMethodDTO', twoFactorAuthenticationMethodId: string, status: string, strategy: string }> | null } | null, currentWorkspace?: { __typename?: 'Workspace', id: string, displayName?: string | null, logo?: string | null, inviteHash?: string | null, allowImpersonation: boolean, activationStatus: WorkspaceActivationStatus, isPublicInviteLinkEnabled: boolean, isGoogleAuthEnabled: boolean, isMicrosoftAuthEnabled: boolean, isPasswordAuthEnabled: boolean, isGoogleAuthBypassEnabled: boolean, isMicrosoftAuthBypassEnabled: boolean, isPasswordAuthBypassEnabled: boolean, subdomain: string, hasValidEnterpriseKey: boolean, isCustomDomainEnabled: boolean, metadataVersion: number, workspaceMembersCount?: number | null, fastModel: string, smartModel: string, isTwoFactorAuthenticationEnforced: boolean, trashRetentionDays: number, editableProfileFields?: Array | null, workspaceCustomApplication?: { __typename?: 'Application', id: string } | null, workspaceUrls: { __typename?: 'WorkspaceUrls', subdomainUrl: string, customUrl?: string | null }, featureFlags?: Array<{ __typename?: 'FeatureFlagDTO', key: FeatureFlagKey, value: boolean }> | null, currentBillingSubscription?: { __typename?: 'BillingSubscription', id: string, status: SubscriptionStatus, interval?: SubscriptionInterval | null, metadata: any, currentPeriodEnd?: string | null, phases: Array<{ __typename?: 'BillingSubscriptionSchedulePhase', start_date: number, end_date: number, items: Array<{ __typename?: 'BillingSubscriptionSchedulePhaseItem', price: string, quantity?: number | null }> }>, billingSubscriptionItems?: Array<{ __typename?: 'BillingSubscriptionItemDTO', id: string, hasReachedCurrentPeriodCap: boolean, quantity?: number | null, stripePriceId: string, billingProduct: { __typename?: 'BillingLicensedProduct', name: string, description: string, images?: Array | null, metadata: { __typename?: 'BillingProductMetadata', productKey: BillingProductKey, planKey: BillingPlanKey, priceUsageBased: BillingUsageType } } | { __typename?: 'BillingMeteredProduct', name: string, description: string, images?: Array | null, metadata: { __typename?: 'BillingProductMetadata', productKey: BillingProductKey, planKey: BillingPlanKey, priceUsageBased: BillingUsageType } } }> | null } | null, billingSubscriptions: Array<{ __typename?: 'BillingSubscription', id: string, status: SubscriptionStatus, metadata: any, phases: Array<{ __typename?: 'BillingSubscriptionSchedulePhase', start_date: number, end_date: number, items: Array<{ __typename?: 'BillingSubscriptionSchedulePhaseItem', price: string, quantity?: number | null }> }> }>, defaultRole?: { __typename?: 'Role', id: string, label: string, description?: string | null, icon?: string | null, canUpdateAllSettings: boolean, canAccessAllTools: boolean, isEditable: boolean, canReadAllObjectRecords: boolean, canUpdateAllObjectRecords: boolean, canSoftDeleteAllObjectRecords: boolean, canDestroyAllObjectRecords: boolean, canBeAssignedToUsers: boolean, canBeAssignedToAgents: boolean, canBeAssignedToApiKeys: boolean } | null } | null, availableWorkspaces: { __typename?: 'AvailableWorkspaces', availableWorkspacesForSignIn: Array<{ __typename?: 'AvailableWorkspace', id: string, displayName?: string | null, loginToken?: string | null, inviteHash?: string | null, personalInviteToken?: string | null, logo?: string | null, workspaceUrls: { __typename?: 'WorkspaceUrls', subdomainUrl: string, customUrl?: string | null }, sso: Array<{ __typename?: 'SSOConnection', type: IdentityProviderType, id: string, issuer: string, name: string, status: SsoIdentityProviderStatus }> }>, availableWorkspacesForSignUp: Array<{ __typename?: 'AvailableWorkspace', id: string, displayName?: string | null, loginToken?: string | null, inviteHash?: string | null, personalInviteToken?: string | null, logo?: string | null, workspaceUrls: { __typename?: 'WorkspaceUrls', subdomainUrl: string, customUrl?: string | null }, sso: Array<{ __typename?: 'SSOConnection', type: IdentityProviderType, id: string, issuer: string, name: string, status: SsoIdentityProviderStatus }> }> } } }; export type ViewFieldFragmentFragment = { __typename?: 'CoreViewField', id: string, fieldMetadataId: string, viewId: string, isVisible: boolean, position: number, size: number, aggregateOperation?: AggregateOperations | null, createdAt: string, updatedAt: string, deletedAt?: string | null }; @@ -7115,31 +7117,6 @@ export const PermissionFlagFragmentFragmentDoc = gql` roleId } `; -export const RowLevelPermissionPredicateFragmentFragmentDoc = gql` - fragment RowLevelPermissionPredicateFragment on RowLevelPermissionPredicate { - id - fieldMetadataId - objectMetadataId - operand - subFieldName - workspaceMemberFieldMetadataId - workspaceMemberSubFieldName - rowLevelPermissionPredicateGroupId - positionInRowLevelPermissionPredicateGroup - roleId - value -} - `; -export const RowLevelPermissionPredicateGroupFragmentFragmentDoc = gql` - fragment RowLevelPermissionPredicateGroupFragment on RowLevelPermissionPredicateGroup { - id - parentRowLevelPermissionPredicateGroupId - logicalOperator - positionInRowLevelPermissionPredicateGroup - roleId - objectMetadataId -} - `; export const WorkspaceMemberQueryFragmentFragmentDoc = gql` fragment WorkspaceMemberQueryFragment on WorkspaceMember { id @@ -7180,6 +7157,31 @@ export const DeletedWorkspaceMemberQueryFragmentFragmentDoc = gql` userEmail } `; +export const RowLevelPermissionPredicateFragmentFragmentDoc = gql` + fragment RowLevelPermissionPredicateFragment on RowLevelPermissionPredicate { + id + fieldMetadataId + objectMetadataId + operand + subFieldName + workspaceMemberFieldMetadataId + workspaceMemberSubFieldName + rowLevelPermissionPredicateGroupId + positionInRowLevelPermissionPredicateGroup + roleId + value +} + `; +export const RowLevelPermissionPredicateGroupFragmentFragmentDoc = gql` + fragment RowLevelPermissionPredicateGroupFragment on RowLevelPermissionPredicateGroup { + id + parentRowLevelPermissionPredicateGroupId + logicalOperator + positionInRowLevelPermissionPredicateGroup + roleId + objectMetadataId +} + `; export const ObjectPermissionFragmentFragmentDoc = gql` fragment ObjectPermissionFragment on ObjectPermission { objectMetadataId @@ -7188,8 +7190,15 @@ export const ObjectPermissionFragmentFragmentDoc = gql` canSoftDeleteObjectRecords canDestroyObjectRecords restrictedFields + rowLevelPermissionPredicates { + ...RowLevelPermissionPredicateFragment + } + rowLevelPermissionPredicateGroups { + ...RowLevelPermissionPredicateGroupFragment + } } - `; + ${RowLevelPermissionPredicateFragmentFragmentDoc} +${RowLevelPermissionPredicateGroupFragmentFragmentDoc}`; export const WorkspaceUrlsFragmentFragmentDoc = gql` fragment WorkspaceUrlsFragment on WorkspaceUrls { subdomainUrl diff --git a/packages/twenty-front/src/generated/graphql.ts b/packages/twenty-front/src/generated/graphql.ts index 76d831d327..074a71d0bf 100644 --- a/packages/twenty-front/src/generated/graphql.ts +++ b/packages/twenty-front/src/generated/graphql.ts @@ -3023,6 +3023,8 @@ export type ObjectPermission = { canUpdateObjectRecords?: Maybe; objectMetadataId: Scalars['UUID']; restrictedFields?: Maybe; + rowLevelPermissionPredicateGroups?: Maybe>; + rowLevelPermissionPredicates?: Maybe>; }; export type ObjectPermissionInput = { diff --git a/packages/twenty-front/src/modules/object-metadata/utils/__tests__/mapObjectMetadataToGraphQLQuery.test.ts b/packages/twenty-front/src/modules/object-metadata/utils/__tests__/mapObjectMetadataToGraphQLQuery.test.ts index ab297b339e..a7c4c039cb 100644 --- a/packages/twenty-front/src/modules/object-metadata/utils/__tests__/mapObjectMetadataToGraphQLQuery.test.ts +++ b/packages/twenty-front/src/modules/object-metadata/utils/__tests__/mapObjectMetadataToGraphQLQuery.test.ts @@ -38,6 +38,8 @@ describe('mapObjectMetadataToGraphQLQuery', () => { canDestroyObjectRecords: true, objectMetadataId: personObjectMetadataItem.id, restrictedFields: {}, + rowLevelPermissionPredicates: [], + rowLevelPermissionPredicateGroups: [], }, }, }); @@ -54,7 +56,7 @@ describe('mapObjectMetadataToGraphQLQuery', () => { primaryEmail additionalEmails } - phone + phone { primaryPhoneNumber primaryPhoneCountryCode @@ -142,6 +144,8 @@ describe('mapObjectMetadataToGraphQLQuery', () => { canDestroyObjectRecords: true, objectMetadataId: personObjectMetadataItem.id, restrictedFields: {}, + rowLevelPermissionPredicates: [], + rowLevelPermissionPredicateGroups: [], }, }, }); diff --git a/packages/twenty-front/src/modules/object-metadata/utils/getObjectPermissionsForObject.ts b/packages/twenty-front/src/modules/object-metadata/utils/getObjectPermissionsForObject.ts index dc96c1bd0a..2f0c267b97 100644 --- a/packages/twenty-front/src/modules/object-metadata/utils/getObjectPermissionsForObject.ts +++ b/packages/twenty-front/src/modules/object-metadata/utils/getObjectPermissionsForObject.ts @@ -22,6 +22,8 @@ export const getObjectPermissionsForObject = ( canDestroyObjectRecords: true, restrictedFields: {}, objectMetadataId, + rowLevelPermissionPredicates: [], + rowLevelPermissionPredicateGroups: [], }; } @@ -33,5 +35,9 @@ export const getObjectPermissionsForObject = ( canDestroyObjectRecords: objectPermissions.canDestroyObjectRecords ?? true, restrictedFields: objectPermissions.restrictedFields ?? {}, objectMetadataId, + rowLevelPermissionPredicates: + objectPermissions.rowLevelPermissionPredicates ?? [], + rowLevelPermissionPredicateGroups: + objectPermissions.rowLevelPermissionPredicateGroups ?? [], }; }; diff --git a/packages/twenty-front/src/modules/object-record/hooks/useBuildRecordInputFromRLSPredicates.ts b/packages/twenty-front/src/modules/object-record/hooks/useBuildRecordInputFromRLSPredicates.ts new file mode 100644 index 0000000000..82439c7d15 --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/hooks/useBuildRecordInputFromRLSPredicates.ts @@ -0,0 +1,121 @@ +import { type ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem'; +import { convertPredicateToRecordFilter } from '@/settings/roles/role-permissions/object-level-permissions/record-level-permissions/utils/recordLevelPermissionPredicateConversion'; + +import { currentWorkspaceMemberState } from '@/auth/states/currentWorkspaceMemberState'; +import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem'; +import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular'; +import { getObjectPermissionsForObject } from '@/object-metadata/utils/getObjectPermissionsForObject'; +import { useFindOneRecord } from '@/object-record/hooks/useFindOneRecord'; +import { useObjectPermissions } from '@/object-record/hooks/useObjectPermissions'; +import { buildRecordInputFromFilter } from '@/object-record/record-table/utils/buildRecordInputFromFilter'; +import { type ObjectRecord } from '@/object-record/types/ObjectRecord'; +import { isUndefined } from '@sniptt/guards'; +import { useRecoilValue } from 'recoil'; +import { RelationType } from 'twenty-shared/types'; +import { isDefined } from 'twenty-shared/utils'; + +export const useBuildRecordInputFromRLSPredicates = ({ + objectMetadataItem, +}: { + objectMetadataItem: ObjectMetadataItem; +}) => { + const currentWorkspaceMember = useRecoilValue(currentWorkspaceMemberState); + + const { record: currentWorkspaceMemberRecord } = useFindOneRecord({ + objectNameSingular: CoreObjectNameSingular.WorkspaceMember, + objectRecordId: currentWorkspaceMember?.id, + }); + + const { objectMetadataItem: workspaceMemberObjectMetadataItem } = + useObjectMetadataItem({ + objectNameSingular: CoreObjectNameSingular.WorkspaceMember, + }); + + const { objectPermissionsByObjectMetadataId } = useObjectPermissions(); + const objectPermissions = getObjectPermissionsForObject( + objectPermissionsByObjectMetadataId, + objectMetadataItem.id, + ); + + const buildRecordInputFromRLSPredicates = (): Partial => { + const recordInput: Partial = {}; + + const rlsPredicates = objectPermissions.rowLevelPermissionPredicates.filter( + (predicate) => predicate.objectMetadataId === objectMetadataItem.id, + ); + + const fieldMetadataItemMap = new Map( + objectMetadataItem.fields.map((field) => [field.id, field]), + ); + + const rlsPredicatesAsRecordFilters = rlsPredicates + .map((predicate) => + convertPredicateToRecordFilter( + predicate, + fieldMetadataItemMap.get(predicate.fieldMetadataId), + ), + ) + .filter(isDefined); + + rlsPredicatesAsRecordFilters.forEach((filter) => { + const fieldMetadataItem = fieldMetadataItemMap.get( + filter.fieldMetadataId, + ); + + if (!isDefined(fieldMetadataItem)) { + return; + } + + if (isDefined(filter.rlsDynamicValue)) { + const workspaceMemberFieldMetadataItem = + workspaceMemberObjectMetadataItem?.fields.find( + (field) => + field.id === + filter.rlsDynamicValue?.workspaceMemberFieldMetadataId, + ); + + if (!isDefined(workspaceMemberFieldMetadataItem)) { + throw new Error( + `Workspace member field metadata item not found for id: ${filter.rlsDynamicValue?.workspaceMemberFieldMetadataId}`, + ); + } + + const recordInputField = + fieldMetadataItem.type === 'RELATION' && + fieldMetadataItem.settings?.relationType === RelationType.MANY_TO_ONE + ? `${fieldMetadataItem.name}Id` + : fieldMetadataItem.name; + + const currentWorkspaceMemberFieldValue = + currentWorkspaceMemberRecord?.[workspaceMemberFieldMetadataItem.name]; + + if (isUndefined(currentWorkspaceMemberFieldValue)) { + throw new Error( + `Current workspace member field value not found for field: ${workspaceMemberFieldMetadataItem.name}`, + ); + } + + recordInput[recordInputField] = currentWorkspaceMemberFieldValue; + } + }); + + // Only process filters without rlsDynamicValue in buildRecordInputFromFilter + // Filters with rlsDynamicValue are already handled above + const staticFilters = rlsPredicatesAsRecordFilters.filter( + (filter) => !isDefined(filter.rlsDynamicValue), + ); + + const recordInputFromFilters = buildRecordInputFromFilter({ + currentRecordFilters: staticFilters, + objectMetadataItem, + currentWorkspaceMember: currentWorkspaceMember ?? undefined, + }); + + return { + ...recordInput, + ...recordInputFromFilters, + }; + }; + + return { buildRecordInputFromRLSPredicates }; +}; diff --git a/packages/twenty-front/src/modules/object-record/record-calendar/month/components/__stories__/RecordCalendarMonth.stories.tsx b/packages/twenty-front/src/modules/object-record/record-calendar/month/components/__stories__/RecordCalendarMonth.stories.tsx index 844f4079c9..e512903f36 100644 --- a/packages/twenty-front/src/modules/object-record/record-calendar/month/components/__stories__/RecordCalendarMonth.stories.tsx +++ b/packages/twenty-front/src/modules/object-record/record-calendar/month/components/__stories__/RecordCalendarMonth.stories.tsx @@ -122,6 +122,8 @@ const meta: Meta = { canSoftDeleteObjectRecords: true, canDestroyObjectRecords: true, restrictedFields: {}, + rowLevelPermissionPredicates: [], + rowLevelPermissionPredicateGroups: [], }, }} > diff --git a/packages/twenty-front/src/modules/object-record/record-filter/utils/isRecordMatchingFilter.ts b/packages/twenty-front/src/modules/object-record/record-filter/utils/isRecordMatchingFilter.ts index 0161e72d91..3f60141ffa 100644 --- a/packages/twenty-front/src/modules/object-record/record-filter/utils/isRecordMatchingFilter.ts +++ b/packages/twenty-front/src/modules/object-record/record-filter/utils/isRecordMatchingFilter.ts @@ -1,20 +1,8 @@ import { isObject } from '@sniptt/guards'; import { type ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem'; -import { isMatchingArrayFilter } from '@/object-record/record-filter/utils/isMatchingArrayFilter'; -import { isMatchingBooleanFilter } from '@/object-record/record-filter/utils/isMatchingBooleanFilter'; -import { isMatchingCurrencyFilter } from '@/object-record/record-filter/utils/isMatchingCurrencyFilter'; -import { isMatchingDateFilter } from '@/object-record/record-filter/utils/isMatchingDateFilter'; -import { isMatchingFloatFilter } from '@/object-record/record-filter/utils/isMatchingFloatFilter'; -import { isMatchingMultiSelectFilter } from '@/object-record/record-filter/utils/isMatchingMultiSelectFilter'; -import { isMatchingRatingFilter } from '@/object-record/record-filter/utils/isMatchingRatingFilter'; -import { isMatchingRawJsonFilter } from '@/object-record/record-filter/utils/isMatchingRawJsonFilter'; -import { isMatchingRichTextV2Filter } from '@/object-record/record-filter/utils/isMatchingRichTextV2Filter'; -import { isMatchingSelectFilter } from '@/object-record/record-filter/utils/isMatchingSelectFilter'; -import { isMatchingStringFilter } from '@/object-record/record-filter/utils/isMatchingStringFilter'; -import { isMatchingTSVectorFilter } from '@/object-record/record-filter/utils/isMatchingTSVectorFilter'; -import { isMatchingUUIDFilter } from '@/object-record/record-filter/utils/isMatchingUUIDFilter'; import { + FieldMetadataType, type ActorFilter, type AddressFilter, type AndObjectRecordFilter, @@ -40,8 +28,23 @@ import { type TSVectorFilter, type UUIDFilter, } from 'twenty-shared/types'; -import { isDefined, isEmptyObject } from 'twenty-shared/utils'; -import { FieldMetadataType } from '~/generated-metadata/graphql'; +import { + isDefined, + isEmptyObject, + isMatchingArrayFilter, + isMatchingBooleanFilter, + isMatchingCurrencyFilter, + isMatchingDateFilter, + isMatchingFloatFilter, + isMatchingMultiSelectFilter, + isMatchingRatingFilter, + isMatchingRawJsonFilter, + isMatchingRichTextV2Filter, + isMatchingSelectFilter, + isMatchingStringFilter, + isMatchingTSVectorFilter, + isMatchingUUIDFilter, +} from 'twenty-shared/utils'; const isLeafFilter = ( filter: RecordGqlOperationFilter, diff --git a/packages/twenty-front/src/modules/object-record/record-show/utils/__tests__/evaluateTabVisibility.test.ts b/packages/twenty-front/src/modules/object-record/record-show/utils/__tests__/evaluateTabVisibility.test.ts index dff8c13ae2..a99494ac20 100644 --- a/packages/twenty-front/src/modules/object-record/record-show/utils/__tests__/evaluateTabVisibility.test.ts +++ b/packages/twenty-front/src/modules/object-record/record-show/utils/__tests__/evaluateTabVisibility.test.ts @@ -49,6 +49,8 @@ const makeObjectPermissions = ( canSoftDeleteObjectRecords: true, canDestroyObjectRecords: true, restrictedFields: {}, + rowLevelPermissionPredicates: [], + rowLevelPermissionPredicateGroups: [], ...overrides, }; }; diff --git a/packages/twenty-front/src/modules/object-record/record-table/hooks/useBuildRecordInputFromFilters.ts b/packages/twenty-front/src/modules/object-record/record-table/hooks/useBuildRecordInputFromFilters.ts index 54585203a4..f68e33fecb 100644 --- a/packages/twenty-front/src/modules/object-record/record-table/hooks/useBuildRecordInputFromFilters.ts +++ b/packages/twenty-front/src/modules/object-record/record-table/hooks/useBuildRecordInputFromFilters.ts @@ -1,12 +1,11 @@ import { currentWorkspaceMemberState } from '@/auth/states/currentWorkspaceMemberState'; import { type ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem'; import { currentRecordFiltersComponentState } from '@/object-record/record-filter/states/currentRecordFiltersComponentState'; -import { buildValueFromFilter } from '@/object-record/record-table/utils/buildRecordInputFromFilter'; +import { buildRecordInputFromFilter } from '@/object-record/record-table/utils/buildRecordInputFromFilter'; import { type ObjectRecord } from '@/object-record/types/ObjectRecord'; import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue'; import { useRecoilValue } from 'recoil'; -import { isDefined } from 'twenty-shared/utils'; export const useBuildRecordInputFromFilters = ({ objectMetadataItem, @@ -21,37 +20,11 @@ export const useBuildRecordInputFromFilters = ({ const currentWorkspaceMember = useRecoilValue(currentWorkspaceMemberState); const buildRecordInputFromFilters = (): Partial => { - const recordInput: Partial = {}; - - currentRecordFilters.forEach((filter) => { - const fieldMetadataItem = objectMetadataItem.fields.find( - (field) => field.id === filter.fieldMetadataId, - ); - - if (!isDefined(fieldMetadataItem)) { - return; - } - if (fieldMetadataItem.type === 'RELATION') { - const value = buildValueFromFilter({ - filter, - options: fieldMetadataItem.options ?? undefined, - relationType: fieldMetadataItem.relation?.type, - currentWorkspaceMember: currentWorkspaceMember ?? undefined, - label: filter.label, - }); - if (!isDefined(value)) { - return; - } - recordInput[`${fieldMetadataItem.name}Id`] = value; - } else { - recordInput[fieldMetadataItem.name] = buildValueFromFilter({ - filter, - options: fieldMetadataItem.options ?? undefined, - }); - } + return buildRecordInputFromFilter({ + currentRecordFilters, + objectMetadataItem, + currentWorkspaceMember: currentWorkspaceMember ?? undefined, }); - - return recordInput; }; return { buildRecordInputFromFilters }; diff --git a/packages/twenty-front/src/modules/object-record/record-table/hooks/useCreateNewIndexRecord.ts b/packages/twenty-front/src/modules/object-record/record-table/hooks/useCreateNewIndexRecord.ts index db19d094ce..c1cd21c0ef 100644 --- a/packages/twenty-front/src/modules/object-record/record-table/hooks/useCreateNewIndexRecord.ts +++ b/packages/twenty-front/src/modules/object-record/record-table/hooks/useCreateNewIndexRecord.ts @@ -2,6 +2,7 @@ import { useCommandMenu } from '@/command-menu/hooks/useCommandMenu'; import { useOpenRecordInCommandMenu } from '@/command-menu/hooks/useOpenRecordInCommandMenu'; import { type ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem'; import { getLabelIdentifierFieldMetadataItem } from '@/object-metadata/utils/getLabelIdentifierFieldMetadataItem'; +import { useBuildRecordInputFromRLSPredicates } from '@/object-record/hooks/useBuildRecordInputFromRLSPredicates'; import { useCreateOneRecord } from '@/object-record/hooks/useCreateOneRecord'; import { recordGroupDefinitionsComponentSelector } from '@/object-record/record-group/states/selectors/recordGroupDefinitionsComponentSelector'; import { recordIndexGroupFieldMetadataItemComponentState } from '@/object-record/record-index/states/recordIndexGroupFieldMetadataComponentState'; @@ -63,10 +64,17 @@ export const useCreateNewIndexRecord = ({ objectMetadataItem, }); + const { buildRecordInputFromRLSPredicates } = + useBuildRecordInputFromRLSPredicates({ + objectMetadataItem, + }); + const createNewIndexRecord = useRecoilCallback( ({ snapshot, set }) => async (recordInput?: Partial) => { const recordId = v4(); + const recordInputFromRLSPredicates = + buildRecordInputFromRLSPredicates(); const recordInputFromFilters = buildRecordInputFromFilters(); const recordIndexOpenRecordIn = snapshot @@ -75,6 +83,7 @@ export const useCreateNewIndexRecord = ({ const createdRecord = await createOneRecord({ id: recordId, + ...recordInputFromRLSPredicates, ...recordInputFromFilters, ...recordInput, }); @@ -163,6 +172,7 @@ export const useCreateNewIndexRecord = ({ return createdRecord; }, [ + buildRecordInputFromRLSPredicates, buildRecordInputFromFilters, createOneRecord, navigate, diff --git a/packages/twenty-front/src/modules/object-record/record-table/utils/buildRecordInputFromFilter.spec.ts b/packages/twenty-front/src/modules/object-record/record-table/utils/__tests__/buildValueFromFilter.spec.ts similarity index 99% rename from packages/twenty-front/src/modules/object-record/record-table/utils/buildRecordInputFromFilter.spec.ts rename to packages/twenty-front/src/modules/object-record/record-table/utils/__tests__/buildValueFromFilter.spec.ts index 9a23800934..432e31c16b 100644 --- a/packages/twenty-front/src/modules/object-record/record-table/utils/buildRecordInputFromFilter.spec.ts +++ b/packages/twenty-front/src/modules/object-record/record-table/utils/__tests__/buildValueFromFilter.spec.ts @@ -1,12 +1,12 @@ import { type FieldMetadataItemOption } from '@/object-metadata/types/FieldMetadataItem'; import { type RecordFilter } from '@/object-record/record-filter/types/RecordFilter'; +import { buildValueFromFilter } from '@/object-record/record-table/utils/buildValueFromFilter'; import { type ColorScheme } from '@/workspace-member/types/WorkspaceMember'; import { type FilterableFieldType, ViewFilterOperand, } from 'twenty-shared/types'; import { RelationType } from '~/generated-metadata/graphql'; -import { buildValueFromFilter } from './buildRecordInputFromFilter'; // TODO: fix the dates, and test the not supported types const mockDate = new Date('2024-03-20T12:00:00Z'); diff --git a/packages/twenty-front/src/modules/object-record/record-table/utils/buildRecordInputFromFilter.ts b/packages/twenty-front/src/modules/object-record/record-table/utils/buildRecordInputFromFilter.ts index b92f5425d5..181dbc5d59 100644 --- a/packages/twenty-front/src/modules/object-record/record-table/utils/buildRecordInputFromFilter.ts +++ b/packages/twenty-front/src/modules/object-record/record-table/utils/buildRecordInputFromFilter.ts @@ -1,335 +1,48 @@ import { type CurrentWorkspaceMember } from '@/auth/states/currentWorkspaceMemberState'; -import { type FieldMetadataItemOption } from '@/object-metadata/types/FieldMetadataItem'; -import { isCompositeFieldType } from '@/object-record/object-filter-dropdown/utils/isCompositeFieldType'; +import { type ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem'; +import { type RecordFilter } from '@/object-record/record-filter/types/RecordFilter'; +import { buildValueFromFilter } from '@/object-record/record-table/utils/buildValueFromFilter'; +import { type ObjectRecord } from 'twenty-shared/types'; +import { isDefined } from 'twenty-shared/utils'; -import { - type RecordFilter, - type RecordFilterToRecordInputOperand, -} from '@/object-record/record-filter/types/RecordFilter'; -import { FILTER_OPERANDS_MAP } from '@/object-record/record-filter/utils/getRecordFilterOperands'; -import { ViewFilterOperand } from 'twenty-shared/types'; -import { assertUnreachable, parseJson } from 'twenty-shared/utils'; -import { RelationType } from '~/generated-metadata/graphql'; - -export const buildValueFromFilter = ({ - filter, - options, - relationType, +export const buildRecordInputFromFilter = ({ + currentRecordFilters, + objectMetadataItem, currentWorkspaceMember, - label, }: { - filter: RecordFilter; - options?: FieldMetadataItemOption[]; - relationType?: RelationType; + currentRecordFilters: RecordFilter[]; + objectMetadataItem: ObjectMetadataItem; currentWorkspaceMember?: CurrentWorkspaceMember; - label?: string; -}) => { - if (isCompositeFieldType(filter.type)) { - return; - } +}): Partial => { + const recordInput: Partial = {}; - if (filter.type === 'RAW_JSON') { - return; - } + currentRecordFilters.forEach((filter) => { + const fieldMetadataItem = objectMetadataItem.fields.find( + (field) => field.id === filter.fieldMetadataId, + ); - const operands = FILTER_OPERANDS_MAP[filter.type]; - if (!operands.some((operand) => operand === filter.operand)) { - throw new Error('Operand not supported for this field type'); - } - - switch (filter.type) { - case 'TEXT': { - return computeValueFromFilterText( - filter.operand as (typeof FILTER_OPERANDS_MAP)['TEXT'][number], - filter.value, - ); + if (!isDefined(fieldMetadataItem)) { + return; } - case 'RATING': - return computeValueFromFilterRating( - filter.operand as (typeof FILTER_OPERANDS_MAP)['RATING'][number], - filter.value, - options, - ); - case 'DATE_TIME': - case 'DATE': - return computeValueFromFilterDate( - filter.operand as (typeof FILTER_OPERANDS_MAP)['DATE_TIME'][number], - filter.value, - ); - case 'NUMBER': - return computeValueFromFilterNumber( - filter.operand as (typeof FILTER_OPERANDS_MAP)['NUMBER'][number], - filter.value, - ); - case 'BOOLEAN': - return computeValueFromFilterBoolean( - filter.operand as (typeof FILTER_OPERANDS_MAP)['BOOLEAN'][number], - filter.value, - ); - case 'TS_VECTOR': - return computeValueFromFilterTSVector( - filter.operand as (typeof FILTER_OPERANDS_MAP)['TS_VECTOR'][number], - filter.value, - ); - case 'ARRAY': - return computeValueFromFilterArray( - filter.operand as (typeof FILTER_OPERANDS_MAP)['ARRAY'][number], - filter.value, - ); - case 'SELECT': - return computeValueFromFilterSelect( - filter.operand as (typeof FILTER_OPERANDS_MAP)['SELECT'][number], - filter.value, - options, - ); - case 'MULTI_SELECT': - return computeValueFromFilterMultiSelect( - filter.operand as (typeof FILTER_OPERANDS_MAP)['MULTI_SELECT'][number], - filter.value, - ); - case 'RELATION': { - return computeValueFromFilterRelation( - filter.operand as (typeof FILTER_OPERANDS_MAP)['RELATION'][number], - filter.value, - relationType, - currentWorkspaceMember, - label, - ); - } - case 'UUID': - return computeValueFromFilterUUID( - filter.operand as (typeof FILTER_OPERANDS_MAP)['UUID'][number], - filter.value, - ); - default: - assertUnreachable(filter.type); - } -}; - -const computeValueFromFilterText = ( - operand: RecordFilterToRecordInputOperand<'TEXT'>, - value: string, -) => { - switch (operand) { - case ViewFilterOperand.CONTAINS: - return value; - case ViewFilterOperand.IS_NOT_EMPTY: - return value; - case ViewFilterOperand.IS_EMPTY: - case ViewFilterOperand.DOES_NOT_CONTAIN: - return undefined; - default: - assertUnreachable(operand); - } -}; - -// TODO: fix this with Temporal -const computeValueFromFilterDate = ( - operand: RecordFilterToRecordInputOperand<'DATE_TIME'>, - value: string, -) => { - switch (operand) { - case ViewFilterOperand.IS: - case ViewFilterOperand.IS_AFTER: - case ViewFilterOperand.IS_BEFORE: - return new Date(value); - case ViewFilterOperand.IS_TODAY: - case ViewFilterOperand.IS_NOT_EMPTY: - case ViewFilterOperand.IS_IN_PAST: - case ViewFilterOperand.IS_IN_FUTURE: - case ViewFilterOperand.IS_RELATIVE: - return new Date(); - case ViewFilterOperand.IS_EMPTY: - return undefined; - default: - assertUnreachable(operand); - } -}; - -const computeValueFromFilterNumber = ( - operand: RecordFilterToRecordInputOperand<'NUMBER'>, - value: string, -) => { - switch (operand) { - //TODO: we shouln't create values from those filters as it makes no sense for the user - case ViewFilterOperand.GREATER_THAN_OR_EQUAL: - return Number(value) + 1; - case ViewFilterOperand.LESS_THAN_OR_EQUAL: - return Number(value) - 1; - case ViewFilterOperand.IS_NOT_EMPTY: - return Number(value); - case ViewFilterOperand.IS: - return Number(value); - case ViewFilterOperand.IS_NOT: - return undefined; - case ViewFilterOperand.IS_EMPTY: - return undefined; - default: - assertUnreachable(operand); - } -}; - -const computeValueFromFilterBoolean = ( - operand: RecordFilterToRecordInputOperand<'BOOLEAN'>, - value: string, -) => { - switch (operand) { - case ViewFilterOperand.IS: - return value === 'true'; - default: - assertUnreachable(operand); - } -}; - -const computeValueFromFilterArray = ( - operand: RecordFilterToRecordInputOperand<'ARRAY'>, - value: string, -) => { - switch (operand) { - case ViewFilterOperand.CONTAINS: - case ViewFilterOperand.IS_NOT_EMPTY: - return value; - case ViewFilterOperand.DOES_NOT_CONTAIN: - case ViewFilterOperand.IS_EMPTY: - return undefined; - default: - assertUnreachable(operand); - } -}; - -const computeValueFromFilterRating = ( - operand: RecordFilterToRecordInputOperand<'RATING'>, - value: string, - options?: FieldMetadataItemOption[], -) => { - const option = options?.find((option) => option.label === value); - if (!option) { - return undefined; - } - - switch (operand) { - case ViewFilterOperand.IS: - case ViewFilterOperand.IS_NOT_EMPTY: - return option.value; - case ViewFilterOperand.GREATER_THAN_OR_EQUAL: { - const plusOne = options?.find( - (opt) => opt.position === option.position + 1, - )?.value; - return plusOne ? plusOne : option.value; - } - case ViewFilterOperand.LESS_THAN_OR_EQUAL: { - const minusOne = options?.find( - (opt) => opt.position === option.position - 1, - )?.value; - return minusOne ? minusOne : option.value; - } - case ViewFilterOperand.IS_EMPTY: - return undefined; - default: - assertUnreachable(operand); - } -}; - -const computeValueFromFilterSelect = ( - operand: RecordFilterToRecordInputOperand<'SELECT'>, - value: string, - options?: FieldMetadataItemOption[], -) => { - switch (operand) { - case ViewFilterOperand.IS: - case ViewFilterOperand.IS_NOT_EMPTY: - try { - const valueParsed = parseJson(value)?.[0]; - const option = options?.find((option) => option.value === valueParsed); - if (!option) { - return undefined; - } - return option.value; - } catch { - return undefined; + if (fieldMetadataItem.type === 'RELATION') { + const value = buildValueFromFilter({ + filter, + options: fieldMetadataItem.options ?? undefined, + relationType: fieldMetadataItem.relation?.type, + currentWorkspaceMember: currentWorkspaceMember ?? undefined, + label: filter.label, + }); + if (!isDefined(value)) { + return; } - case ViewFilterOperand.IS_NOT: - case ViewFilterOperand.IS_EMPTY: - return undefined; - default: - assertUnreachable(operand); - } -}; - -const computeValueFromFilterMultiSelect = ( - operand: RecordFilterToRecordInputOperand<'MULTI_SELECT'>, - value: string, -) => { - switch (operand) { - case ViewFilterOperand.CONTAINS: - case ViewFilterOperand.IS_NOT_EMPTY: - try { - const parsedValue = parseJson(value); - return parsedValue ? parsedValue : undefined; - } catch { - return undefined; - } - case ViewFilterOperand.DOES_NOT_CONTAIN: - case ViewFilterOperand.IS_EMPTY: - return undefined; - default: - assertUnreachable(operand); - } -}; - -const computeValueFromFilterRelation = ( - operand: RecordFilterToRecordInputOperand<'RELATION'>, - value: string, - relationType?: RelationType, - currentWorkspaceMember?: CurrentWorkspaceMember, - label?: string, -) => { - switch (operand) { - case ViewFilterOperand.IS: { - const parsedValue = parseJson<{ - isCurrentWorkspaceMemberSelected: boolean; - selectedRecordIds: string[]; - }>(value); - if (relationType === RelationType.MANY_TO_ONE) { - if (label === 'Assignee') { - return parsedValue?.isCurrentWorkspaceMemberSelected - ? currentWorkspaceMember?.id - : undefined; - } else { - return parsedValue?.selectedRecordIds?.[0]; - } - } - return undefined; //todo + recordInput[`${fieldMetadataItem.name}Id`] = value; + } else { + recordInput[fieldMetadataItem.name] = buildValueFromFilter({ + filter, + options: fieldMetadataItem.options ?? undefined, + }); } - case ViewFilterOperand.IS_NOT: - case ViewFilterOperand.IS_NOT_EMPTY: // todo - case ViewFilterOperand.IS_EMPTY: - return undefined; - default: - assertUnreachable(operand); - } -}; + }); -const computeValueFromFilterTSVector = ( - operand: RecordFilterToRecordInputOperand<'TS_VECTOR'>, - value: string, -) => { - switch (operand) { - case ViewFilterOperand.VECTOR_SEARCH: - return value; - default: - assertUnreachable(operand); - } -}; - -const computeValueFromFilterUUID = ( - operand: RecordFilterToRecordInputOperand<'UUID'>, - value: string, -) => { - switch (operand) { - case ViewFilterOperand.IS: - return value; - default: - assertUnreachable(operand); - } + return recordInput; }; diff --git a/packages/twenty-front/src/modules/object-record/record-table/utils/buildValueFromFilter.ts b/packages/twenty-front/src/modules/object-record/record-table/utils/buildValueFromFilter.ts new file mode 100644 index 0000000000..b92f5425d5 --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/record-table/utils/buildValueFromFilter.ts @@ -0,0 +1,335 @@ +import { type CurrentWorkspaceMember } from '@/auth/states/currentWorkspaceMemberState'; +import { type FieldMetadataItemOption } from '@/object-metadata/types/FieldMetadataItem'; +import { isCompositeFieldType } from '@/object-record/object-filter-dropdown/utils/isCompositeFieldType'; + +import { + type RecordFilter, + type RecordFilterToRecordInputOperand, +} from '@/object-record/record-filter/types/RecordFilter'; +import { FILTER_OPERANDS_MAP } from '@/object-record/record-filter/utils/getRecordFilterOperands'; +import { ViewFilterOperand } from 'twenty-shared/types'; +import { assertUnreachable, parseJson } from 'twenty-shared/utils'; +import { RelationType } from '~/generated-metadata/graphql'; + +export const buildValueFromFilter = ({ + filter, + options, + relationType, + currentWorkspaceMember, + label, +}: { + filter: RecordFilter; + options?: FieldMetadataItemOption[]; + relationType?: RelationType; + currentWorkspaceMember?: CurrentWorkspaceMember; + label?: string; +}) => { + if (isCompositeFieldType(filter.type)) { + return; + } + + if (filter.type === 'RAW_JSON') { + return; + } + + const operands = FILTER_OPERANDS_MAP[filter.type]; + if (!operands.some((operand) => operand === filter.operand)) { + throw new Error('Operand not supported for this field type'); + } + + switch (filter.type) { + case 'TEXT': { + return computeValueFromFilterText( + filter.operand as (typeof FILTER_OPERANDS_MAP)['TEXT'][number], + filter.value, + ); + } + case 'RATING': + return computeValueFromFilterRating( + filter.operand as (typeof FILTER_OPERANDS_MAP)['RATING'][number], + filter.value, + options, + ); + case 'DATE_TIME': + case 'DATE': + return computeValueFromFilterDate( + filter.operand as (typeof FILTER_OPERANDS_MAP)['DATE_TIME'][number], + filter.value, + ); + case 'NUMBER': + return computeValueFromFilterNumber( + filter.operand as (typeof FILTER_OPERANDS_MAP)['NUMBER'][number], + filter.value, + ); + case 'BOOLEAN': + return computeValueFromFilterBoolean( + filter.operand as (typeof FILTER_OPERANDS_MAP)['BOOLEAN'][number], + filter.value, + ); + case 'TS_VECTOR': + return computeValueFromFilterTSVector( + filter.operand as (typeof FILTER_OPERANDS_MAP)['TS_VECTOR'][number], + filter.value, + ); + case 'ARRAY': + return computeValueFromFilterArray( + filter.operand as (typeof FILTER_OPERANDS_MAP)['ARRAY'][number], + filter.value, + ); + case 'SELECT': + return computeValueFromFilterSelect( + filter.operand as (typeof FILTER_OPERANDS_MAP)['SELECT'][number], + filter.value, + options, + ); + case 'MULTI_SELECT': + return computeValueFromFilterMultiSelect( + filter.operand as (typeof FILTER_OPERANDS_MAP)['MULTI_SELECT'][number], + filter.value, + ); + case 'RELATION': { + return computeValueFromFilterRelation( + filter.operand as (typeof FILTER_OPERANDS_MAP)['RELATION'][number], + filter.value, + relationType, + currentWorkspaceMember, + label, + ); + } + case 'UUID': + return computeValueFromFilterUUID( + filter.operand as (typeof FILTER_OPERANDS_MAP)['UUID'][number], + filter.value, + ); + default: + assertUnreachable(filter.type); + } +}; + +const computeValueFromFilterText = ( + operand: RecordFilterToRecordInputOperand<'TEXT'>, + value: string, +) => { + switch (operand) { + case ViewFilterOperand.CONTAINS: + return value; + case ViewFilterOperand.IS_NOT_EMPTY: + return value; + case ViewFilterOperand.IS_EMPTY: + case ViewFilterOperand.DOES_NOT_CONTAIN: + return undefined; + default: + assertUnreachable(operand); + } +}; + +// TODO: fix this with Temporal +const computeValueFromFilterDate = ( + operand: RecordFilterToRecordInputOperand<'DATE_TIME'>, + value: string, +) => { + switch (operand) { + case ViewFilterOperand.IS: + case ViewFilterOperand.IS_AFTER: + case ViewFilterOperand.IS_BEFORE: + return new Date(value); + case ViewFilterOperand.IS_TODAY: + case ViewFilterOperand.IS_NOT_EMPTY: + case ViewFilterOperand.IS_IN_PAST: + case ViewFilterOperand.IS_IN_FUTURE: + case ViewFilterOperand.IS_RELATIVE: + return new Date(); + case ViewFilterOperand.IS_EMPTY: + return undefined; + default: + assertUnreachable(operand); + } +}; + +const computeValueFromFilterNumber = ( + operand: RecordFilterToRecordInputOperand<'NUMBER'>, + value: string, +) => { + switch (operand) { + //TODO: we shouln't create values from those filters as it makes no sense for the user + case ViewFilterOperand.GREATER_THAN_OR_EQUAL: + return Number(value) + 1; + case ViewFilterOperand.LESS_THAN_OR_EQUAL: + return Number(value) - 1; + case ViewFilterOperand.IS_NOT_EMPTY: + return Number(value); + case ViewFilterOperand.IS: + return Number(value); + case ViewFilterOperand.IS_NOT: + return undefined; + case ViewFilterOperand.IS_EMPTY: + return undefined; + default: + assertUnreachable(operand); + } +}; + +const computeValueFromFilterBoolean = ( + operand: RecordFilterToRecordInputOperand<'BOOLEAN'>, + value: string, +) => { + switch (operand) { + case ViewFilterOperand.IS: + return value === 'true'; + default: + assertUnreachable(operand); + } +}; + +const computeValueFromFilterArray = ( + operand: RecordFilterToRecordInputOperand<'ARRAY'>, + value: string, +) => { + switch (operand) { + case ViewFilterOperand.CONTAINS: + case ViewFilterOperand.IS_NOT_EMPTY: + return value; + case ViewFilterOperand.DOES_NOT_CONTAIN: + case ViewFilterOperand.IS_EMPTY: + return undefined; + default: + assertUnreachable(operand); + } +}; + +const computeValueFromFilterRating = ( + operand: RecordFilterToRecordInputOperand<'RATING'>, + value: string, + options?: FieldMetadataItemOption[], +) => { + const option = options?.find((option) => option.label === value); + if (!option) { + return undefined; + } + + switch (operand) { + case ViewFilterOperand.IS: + case ViewFilterOperand.IS_NOT_EMPTY: + return option.value; + case ViewFilterOperand.GREATER_THAN_OR_EQUAL: { + const plusOne = options?.find( + (opt) => opt.position === option.position + 1, + )?.value; + return plusOne ? plusOne : option.value; + } + case ViewFilterOperand.LESS_THAN_OR_EQUAL: { + const minusOne = options?.find( + (opt) => opt.position === option.position - 1, + )?.value; + return minusOne ? minusOne : option.value; + } + case ViewFilterOperand.IS_EMPTY: + return undefined; + default: + assertUnreachable(operand); + } +}; + +const computeValueFromFilterSelect = ( + operand: RecordFilterToRecordInputOperand<'SELECT'>, + value: string, + options?: FieldMetadataItemOption[], +) => { + switch (operand) { + case ViewFilterOperand.IS: + case ViewFilterOperand.IS_NOT_EMPTY: + try { + const valueParsed = parseJson(value)?.[0]; + const option = options?.find((option) => option.value === valueParsed); + if (!option) { + return undefined; + } + return option.value; + } catch { + return undefined; + } + case ViewFilterOperand.IS_NOT: + case ViewFilterOperand.IS_EMPTY: + return undefined; + default: + assertUnreachable(operand); + } +}; + +const computeValueFromFilterMultiSelect = ( + operand: RecordFilterToRecordInputOperand<'MULTI_SELECT'>, + value: string, +) => { + switch (operand) { + case ViewFilterOperand.CONTAINS: + case ViewFilterOperand.IS_NOT_EMPTY: + try { + const parsedValue = parseJson(value); + return parsedValue ? parsedValue : undefined; + } catch { + return undefined; + } + case ViewFilterOperand.DOES_NOT_CONTAIN: + case ViewFilterOperand.IS_EMPTY: + return undefined; + default: + assertUnreachable(operand); + } +}; + +const computeValueFromFilterRelation = ( + operand: RecordFilterToRecordInputOperand<'RELATION'>, + value: string, + relationType?: RelationType, + currentWorkspaceMember?: CurrentWorkspaceMember, + label?: string, +) => { + switch (operand) { + case ViewFilterOperand.IS: { + const parsedValue = parseJson<{ + isCurrentWorkspaceMemberSelected: boolean; + selectedRecordIds: string[]; + }>(value); + if (relationType === RelationType.MANY_TO_ONE) { + if (label === 'Assignee') { + return parsedValue?.isCurrentWorkspaceMemberSelected + ? currentWorkspaceMember?.id + : undefined; + } else { + return parsedValue?.selectedRecordIds?.[0]; + } + } + return undefined; //todo + } + case ViewFilterOperand.IS_NOT: + case ViewFilterOperand.IS_NOT_EMPTY: // todo + case ViewFilterOperand.IS_EMPTY: + return undefined; + default: + assertUnreachable(operand); + } +}; + +const computeValueFromFilterTSVector = ( + operand: RecordFilterToRecordInputOperand<'TS_VECTOR'>, + value: string, +) => { + switch (operand) { + case ViewFilterOperand.VECTOR_SEARCH: + return value; + default: + assertUnreachable(operand); + } +}; + +const computeValueFromFilterUUID = ( + operand: RecordFilterToRecordInputOperand<'UUID'>, + value: string, +) => { + switch (operand) { + case ViewFilterOperand.IS: + return value; + default: + assertUnreachable(operand); + } +}; diff --git a/packages/twenty-front/src/modules/page-layout/utils/__tests__/checkFieldPermissions.test.ts b/packages/twenty-front/src/modules/page-layout/utils/__tests__/checkFieldPermissions.test.ts index 29a7fcb179..2d74b69de1 100644 --- a/packages/twenty-front/src/modules/page-layout/utils/__tests__/checkFieldPermissions.test.ts +++ b/packages/twenty-front/src/modules/page-layout/utils/__tests__/checkFieldPermissions.test.ts @@ -1,5 +1,5 @@ -import { type ObjectPermissions } from 'twenty-shared/types'; import { checkFieldPermissions } from '@/page-layout/utils/checkFieldPermissions'; +import { type ObjectPermissions } from 'twenty-shared/types'; describe('checkFieldPermissions', () => { const mockObjectPermissions: ObjectPermissions = { @@ -8,6 +8,8 @@ describe('checkFieldPermissions', () => { canSoftDeleteObjectRecords: true, canDestroyObjectRecords: true, restrictedFields: {}, + rowLevelPermissionPredicates: [], + rowLevelPermissionPredicateGroups: [], }; it('should return true when no fields are restricted', () => { diff --git a/packages/twenty-front/src/modules/page-layout/widgets/components/__stories__/WidgetRenderer.stories.tsx b/packages/twenty-front/src/modules/page-layout/widgets/components/__stories__/WidgetRenderer.stories.tsx index 4510561c56..b723e4e549 100644 --- a/packages/twenty-front/src/modules/page-layout/widgets/components/__stories__/WidgetRenderer.stories.tsx +++ b/packages/twenty-front/src/modules/page-layout/widgets/components/__stories__/WidgetRenderer.stories.tsx @@ -1537,6 +1537,8 @@ export const Catalog: CatalogStory = { canSoftDeleteObjectRecords: false, canDestroyObjectRecords: false, restrictedFields: {}, + rowLevelPermissionPredicates: [], + rowLevelPermissionPredicateGroups: [], }, ], }); @@ -1552,6 +1554,8 @@ export const Catalog: CatalogStory = { canSoftDeleteObjectRecords: true, canDestroyObjectRecords: true, restrictedFields: {}, + rowLevelPermissionPredicates: [], + rowLevelPermissionPredicateGroups: [], }, ], }); diff --git a/packages/twenty-front/src/modules/settings/roles/graphql/fragments/objectPermissionFragment.ts b/packages/twenty-front/src/modules/settings/roles/graphql/fragments/objectPermissionFragment.ts index fe137d2766..2964a3827c 100644 --- a/packages/twenty-front/src/modules/settings/roles/graphql/fragments/objectPermissionFragment.ts +++ b/packages/twenty-front/src/modules/settings/roles/graphql/fragments/objectPermissionFragment.ts @@ -1,3 +1,5 @@ +import { ROW_LEVEL_PERMISSION_PREDICATE_FRAGMENT } from '@/settings/roles/graphql/fragments/rowLevelPermissionPredicateFragment'; +import { ROW_LEVEL_PERMISSION_PREDICATE_GROUP_FRAGMENT } from '@/settings/roles/graphql/fragments/rowLevelPermissionPredicateGroupFragment'; import { gql } from '@apollo/client'; export const OBJECT_PERMISSION_FRAGMENT = gql` @@ -8,5 +10,13 @@ export const OBJECT_PERMISSION_FRAGMENT = gql` canSoftDeleteObjectRecords canDestroyObjectRecords restrictedFields + rowLevelPermissionPredicates { + ...RowLevelPermissionPredicateFragment + } + rowLevelPermissionPredicateGroups { + ...RowLevelPermissionPredicateGroupFragment + } } + ${ROW_LEVEL_PERMISSION_PREDICATE_FRAGMENT} + ${ROW_LEVEL_PERMISSION_PREDICATE_GROUP_FRAGMENT} `; diff --git a/packages/twenty-front/src/modules/settings/roles/role-permissions/objects-permissions/utils/getObjectPermissionsFromMapByObjectMetadataId.ts b/packages/twenty-front/src/modules/settings/roles/role-permissions/objects-permissions/utils/getObjectPermissionsFromMapByObjectMetadataId.ts index 8b7345a7a9..102592e877 100644 --- a/packages/twenty-front/src/modules/settings/roles/role-permissions/objects-permissions/utils/getObjectPermissionsFromMapByObjectMetadataId.ts +++ b/packages/twenty-front/src/modules/settings/roles/role-permissions/objects-permissions/utils/getObjectPermissionsFromMapByObjectMetadataId.ts @@ -19,6 +19,8 @@ export const getObjectPermissionsFromMapByObjectMetadataId = ({ canCreateObjectRecords: true, canDeleteObjectRecords: true, restrictedFields: {}, + rowLevelPermissionPredicates: [], + rowLevelPermissionPredicateGroups: [], } ); }; diff --git a/packages/twenty-front/src/testing/mock-data/users.ts b/packages/twenty-front/src/testing/mock-data/users.ts index a4aa5a5da5..82a8f101cb 100644 --- a/packages/twenty-front/src/testing/mock-data/users.ts +++ b/packages/twenty-front/src/testing/mock-data/users.ts @@ -199,6 +199,8 @@ export const mockedUserData: MockedUser = { canSoftDeleteObjectRecords: true, canDestroyObjectRecords: true, restrictedFields: {}, + rowLevelPermissionPredicates: [], + rowLevelPermissionPredicateGroups: [], })), }, locale: 'en', @@ -229,6 +231,8 @@ export const mockedLimitedPermissionsUserData: MockedUser = { canSoftDeleteObjectRecords: true, canDestroyObjectRecords: true, restrictedFields: {}, + rowLevelPermissionPredicates: [], + rowLevelPermissionPredicateGroups: [], })), }, }; @@ -259,6 +263,8 @@ export const mockedOnboardingUserData = ( canSoftDeleteObjectRecords: true, canDestroyObjectRecords: true, restrictedFields: {}, + rowLevelPermissionPredicates: [], + rowLevelPermissionPredicateGroups: [], })), }, locale: 'en', diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-entity/constant/all-metadata-relations.constant.ts b/packages/twenty-server/src/engine/metadata-modules/flat-entity/constant/all-metadata-relations.constant.ts index d57c8a301c..f9ed0f0b1c 100644 --- a/packages/twenty-server/src/engine/metadata-modules/flat-entity/constant/all-metadata-relations.constant.ts +++ b/packages/twenty-server/src/engine/metadata-modules/flat-entity/constant/all-metadata-relations.constant.ts @@ -284,6 +284,12 @@ export const ALL_METADATA_RELATIONS = { objectPermissions: null, permissionFlags: null, fieldPermissions: null, + rowLevelPermissionPredicates: { + metadataName: 'rowLevelPermissionPredicate', + }, + rowLevelPermissionPredicateGroups: { + metadataName: 'rowLevelPermissionPredicateGroup', + }, }, }, roleTarget: { diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-entity/utils/__tests__/__snapshots__/get-metadata-related-metadata-names.util.spec.ts.snap b/packages/twenty-server/src/engine/metadata-modules/flat-entity/utils/__tests__/__snapshots__/get-metadata-related-metadata-names.util.spec.ts.snap index 86fb1c790f..5474ef65f5 100644 --- a/packages/twenty-server/src/engine/metadata-modules/flat-entity/utils/__tests__/__snapshots__/get-metadata-related-metadata-names.util.spec.ts.snap +++ b/packages/twenty-server/src/engine/metadata-modules/flat-entity/utils/__tests__/__snapshots__/get-metadata-related-metadata-names.util.spec.ts.snap @@ -62,6 +62,8 @@ exports[`getMetadataRelatedMetadataNames should return related metadata names fo exports[`getMetadataRelatedMetadataNames should return related metadata names for role 1`] = ` [ "roleTarget", + "rowLevelPermissionPredicate", + "rowLevelPermissionPredicateGroup", ] `; diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-role/utils/from-create-role-input-to-flat-role-to-create.util.ts b/packages/twenty-server/src/engine/metadata-modules/flat-role/utils/from-create-role-input-to-flat-role-to-create.util.ts index 91b8c5c88d..aae26f68f4 100644 --- a/packages/twenty-server/src/engine/metadata-modules/flat-role/utils/from-create-role-input-to-flat-role-to-create.util.ts +++ b/packages/twenty-server/src/engine/metadata-modules/flat-role/utils/from-create-role-input-to-flat-role-to-create.util.ts @@ -54,5 +54,7 @@ export const fromCreateRoleInputToFlatRoleToCreate = ({ objectPermissionIds: [], permissionFlagIds: [], fieldPermissionIds: [], + rowLevelPermissionPredicateIds: [], + rowLevelPermissionPredicateGroupIds: [], }; }; diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-role/utils/from-role-entity-to-flat-role.util.ts b/packages/twenty-server/src/engine/metadata-modules/flat-role/utils/from-role-entity-to-flat-role.util.ts index 8eb248179b..58fb046d25 100644 --- a/packages/twenty-server/src/engine/metadata-modules/flat-role/utils/from-role-entity-to-flat-role.util.ts +++ b/packages/twenty-server/src/engine/metadata-modules/flat-role/utils/from-role-entity-to-flat-role.util.ts @@ -27,5 +27,10 @@ export const fromRoleEntityToFlatRole = (role: RoleEntity): FlatRole => { objectPermissionIds: role.objectPermissions.map((op) => op.id), permissionFlagIds: role.permissionFlags.map((pf) => pf.id), fieldPermissionIds: role.fieldPermissions.map((fp) => fp.id), + rowLevelPermissionPredicateIds: role.rowLevelPermissionPredicates.map( + (rp) => rp.id, + ), + rowLevelPermissionPredicateGroupIds: + role.rowLevelPermissionPredicateGroups.map((rp) => rp.id), }; }; diff --git a/packages/twenty-server/src/engine/metadata-modules/object-permission/dtos/object-permission.dto.ts b/packages/twenty-server/src/engine/metadata-modules/object-permission/dtos/object-permission.dto.ts index c99c83ae4e..124ab88fa2 100644 --- a/packages/twenty-server/src/engine/metadata-modules/object-permission/dtos/object-permission.dto.ts +++ b/packages/twenty-server/src/engine/metadata-modules/object-permission/dtos/object-permission.dto.ts @@ -4,6 +4,8 @@ import GraphQLJSON from 'graphql-type-json'; import { RestrictedFieldsPermissions } from 'twenty-shared/types'; import { UUIDScalarType } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars'; +import { RowLevelPermissionPredicateGroupDTO } from 'src/engine/metadata-modules/row-level-permission-predicate/dtos/row-level-permission-predicate-group.dto'; +import { RowLevelPermissionPredicateDTO } from 'src/engine/metadata-modules/row-level-permission-predicate/dtos/row-level-permission-predicate.dto'; @ObjectType('ObjectPermission') export class ObjectPermissionDTO { @@ -26,4 +28,10 @@ export class ObjectPermissionDTO { nullable: true, }) restrictedFields?: RestrictedFieldsPermissions; + + @Field(() => [RowLevelPermissionPredicateDTO], { nullable: true }) + rowLevelPermissionPredicates?: RowLevelPermissionPredicateDTO[]; + + @Field(() => [RowLevelPermissionPredicateGroupDTO], { nullable: true }) + rowLevelPermissionPredicateGroups?: RowLevelPermissionPredicateGroupDTO[]; } diff --git a/packages/twenty-server/src/engine/metadata-modules/object-permission/field-permission/__tests__/field-permissions.service.spec.ts b/packages/twenty-server/src/engine/metadata-modules/object-permission/field-permission/__tests__/field-permissions.service.spec.ts index 82c744d5e8..d8f338bfdf 100644 --- a/packages/twenty-server/src/engine/metadata-modules/object-permission/field-permission/__tests__/field-permissions.service.spec.ts +++ b/packages/twenty-server/src/engine/metadata-modules/object-permission/field-permission/__tests__/field-permissions.service.spec.ts @@ -61,6 +61,8 @@ describe('FieldPermissionService', () => { canSoftDeleteObjectRecords: false, canDestroyObjectRecords: false, restrictedFields: {}, + rowLevelPermissionPredicates: [], + rowLevelPermissionPredicateGroups: [], }, [fieldRelationMock.objectMetadataId]: { canReadObjectRecords: true, @@ -68,6 +70,8 @@ describe('FieldPermissionService', () => { canSoftDeleteObjectRecords: false, canDestroyObjectRecords: false, restrictedFields: {}, + rowLevelPermissionPredicates: [], + rowLevelPermissionPredicateGroups: [], }, }, }; diff --git a/packages/twenty-server/src/engine/metadata-modules/role/role.entity.ts b/packages/twenty-server/src/engine/metadata-modules/role/role.entity.ts index 7152b12dc7..7561620d2e 100644 --- a/packages/twenty-server/src/engine/metadata-modules/role/role.entity.ts +++ b/packages/twenty-server/src/engine/metadata-modules/role/role.entity.ts @@ -13,6 +13,8 @@ import { FieldPermissionEntity } from 'src/engine/metadata-modules/object-permis import { ObjectPermissionEntity } from 'src/engine/metadata-modules/object-permission/object-permission.entity'; import { PermissionFlagEntity } from 'src/engine/metadata-modules/permission-flag/permission-flag.entity'; import { RoleTargetEntity } from 'src/engine/metadata-modules/role-target/role-target.entity'; +import { RowLevelPermissionPredicateGroupEntity } from 'src/engine/metadata-modules/row-level-permission-predicate/entities/row-level-permission-predicate-group.entity'; +import { RowLevelPermissionPredicateEntity } from 'src/engine/metadata-modules/row-level-permission-predicate/entities/row-level-permission-predicate.entity'; import { SyncableEntity } from 'src/engine/workspace-manager/types/syncable-entity.interface'; @Entity('role') @@ -92,4 +94,21 @@ export class RoleEntity extends SyncableEntity implements Required { (fieldPermission: FieldPermissionEntity) => fieldPermission.role, ) fieldPermissions: Relation; + + @OneToMany( + () => RowLevelPermissionPredicateEntity, + (rowLevelPermissionPredicate: RowLevelPermissionPredicateEntity) => + rowLevelPermissionPredicate.role, + ) + rowLevelPermissionPredicates: Relation; + + @OneToMany( + () => RowLevelPermissionPredicateGroupEntity, + ( + rowLevelPermissionPredicateGroup: RowLevelPermissionPredicateGroupEntity, + ) => rowLevelPermissionPredicateGroup.role, + ) + rowLevelPermissionPredicateGroups: Relation< + RowLevelPermissionPredicateGroupEntity[] + >; } diff --git a/packages/twenty-server/src/engine/metadata-modules/role/role.module.ts b/packages/twenty-server/src/engine/metadata-modules/role/role.module.ts index eac60e3f55..f94d8c0860 100644 --- a/packages/twenty-server/src/engine/metadata-modules/role/role.module.ts +++ b/packages/twenty-server/src/engine/metadata-modules/role/role.module.ts @@ -17,13 +17,15 @@ import { ObjectPermissionModule } from 'src/engine/metadata-modules/object-permi import { PermissionFlagEntity } from 'src/engine/metadata-modules/permission-flag/permission-flag.entity'; import { PermissionFlagModule } from 'src/engine/metadata-modules/permission-flag/permission-flag.module'; import { PermissionsModule } from 'src/engine/metadata-modules/permissions/permissions.module'; -import { RowLevelPermissionModule } from 'src/engine/metadata-modules/row-level-permission-predicate/row-level-permission.module'; import { RoleTargetEntity } from 'src/engine/metadata-modules/role-target/role-target.entity'; import { RoleEntity } from 'src/engine/metadata-modules/role/role.entity'; import { RoleResolver } from 'src/engine/metadata-modules/role/role.resolver'; import { RoleService } from 'src/engine/metadata-modules/role/role.service'; import { WorkspaceFlatRoleMapCacheService } from 'src/engine/metadata-modules/role/services/workspace-flat-role-map-cache.service'; import { WorkspaceRolesPermissionsCacheService } from 'src/engine/metadata-modules/role/services/workspace-roles-permissions-cache.service'; +import { RowLevelPermissionPredicateGroupEntity } from 'src/engine/metadata-modules/row-level-permission-predicate/entities/row-level-permission-predicate-group.entity'; +import { RowLevelPermissionPredicateEntity } from 'src/engine/metadata-modules/row-level-permission-predicate/entities/row-level-permission-predicate.entity'; +import { RowLevelPermissionModule } from 'src/engine/metadata-modules/row-level-permission-predicate/row-level-permission.module'; import { UserRoleModule } from 'src/engine/metadata-modules/user-role/user-role.module'; import { WorkspaceCacheModule } from 'src/engine/workspace-cache/workspace-cache.module'; import { WorkspaceMigrationBuilderGraphqlApiExceptionInterceptor } from 'src/engine/workspace-manager/workspace-migration/interceptors/workspace-migration-builder-graphql-api-exception.interceptor'; @@ -39,6 +41,8 @@ import { WorkspaceMigrationModule } from 'src/engine/workspace-manager/workspace FieldPermissionEntity, UserWorkspaceEntity, ObjectMetadataEntity, + RowLevelPermissionPredicateEntity, + RowLevelPermissionPredicateGroupEntity, ]), UserRoleModule, AiAgentRoleModule, diff --git a/packages/twenty-server/src/engine/metadata-modules/role/services/workspace-flat-role-map-cache.service.ts b/packages/twenty-server/src/engine/metadata-modules/role/services/workspace-flat-role-map-cache.service.ts index 4dbf04ec77..bca8f0804a 100644 --- a/packages/twenty-server/src/engine/metadata-modules/role/services/workspace-flat-role-map-cache.service.ts +++ b/packages/twenty-server/src/engine/metadata-modules/role/services/workspace-flat-role-map-cache.service.ts @@ -14,6 +14,8 @@ import { ObjectPermissionEntity } from 'src/engine/metadata-modules/object-permi import { PermissionFlagEntity } from 'src/engine/metadata-modules/permission-flag/permission-flag.entity'; import { RoleTargetEntity } from 'src/engine/metadata-modules/role-target/role-target.entity'; import { RoleEntity } from 'src/engine/metadata-modules/role/role.entity'; +import { RowLevelPermissionPredicateGroupEntity } from 'src/engine/metadata-modules/row-level-permission-predicate/entities/row-level-permission-predicate-group.entity'; +import { RowLevelPermissionPredicateEntity } from 'src/engine/metadata-modules/row-level-permission-predicate/entities/row-level-permission-predicate.entity'; import { WorkspaceCache } from 'src/engine/workspace-cache/decorators/workspace-cache.decorator'; import { regroupEntitiesByRelatedEntityId } from 'src/engine/workspace-cache/utils/regroup-entities-by-related-entity-id'; import { addFlatEntityToFlatEntityMapsThroughMutationOrThrow } from 'src/engine/workspace-manager/workspace-migration/utils/add-flat-entity-to-flat-entity-maps-through-mutation-or-throw.util'; @@ -34,6 +36,10 @@ export class WorkspaceFlatRoleMapCacheService extends WorkspaceCacheProvider< private readonly permissionFlagRepository: Repository, @InjectRepository(FieldPermissionEntity) private readonly fieldPermissionRepository: Repository, + @InjectRepository(RowLevelPermissionPredicateEntity) + private readonly rowLevelPermissionPredicateRepository: Repository, + @InjectRepository(RowLevelPermissionPredicateGroupEntity) + private readonly rowLevelPermissionPredicateGroupRepository: Repository, ) { super(); } @@ -47,6 +53,8 @@ export class WorkspaceFlatRoleMapCacheService extends WorkspaceCacheProvider< objectPermissions, permissionFlags, fieldPermissions, + rowLevelPermissionPredicates, + rowLevelPermissionPredicateGroups, ] = await Promise.all([ this.roleRepository.find({ where: { workspaceId }, @@ -72,6 +80,16 @@ export class WorkspaceFlatRoleMapCacheService extends WorkspaceCacheProvider< select: ['id', 'roleId'], withDeleted: true, }), + this.rowLevelPermissionPredicateRepository.find({ + where: { workspaceId }, + select: ['id', 'roleId'], + withDeleted: true, + }), + this.rowLevelPermissionPredicateGroupRepository.find({ + where: { workspaceId }, + select: ['id', 'roleId'], + withDeleted: true, + }), ]); const [ @@ -79,6 +97,8 @@ export class WorkspaceFlatRoleMapCacheService extends WorkspaceCacheProvider< objectPermissionsByRoleId, permissionFlagsByRoleId, fieldPermissionsByRoleId, + rowLevelPermissionPredicatesByRoleId, + rowLevelPermissionPredicateGroupsByRoleId, ] = ( [ { @@ -97,6 +117,14 @@ export class WorkspaceFlatRoleMapCacheService extends WorkspaceCacheProvider< entities: fieldPermissions, foreignKey: 'roleId', }, + { + entities: rowLevelPermissionPredicates, + foreignKey: 'roleId', + }, + { + entities: rowLevelPermissionPredicateGroups, + foreignKey: 'roleId', + }, ] as const ).map(regroupEntitiesByRelatedEntityId); @@ -109,6 +137,10 @@ export class WorkspaceFlatRoleMapCacheService extends WorkspaceCacheProvider< objectPermissions: objectPermissionsByRoleId.get(roleEntity.id) || [], permissionFlags: permissionFlagsByRoleId.get(roleEntity.id) || [], fieldPermissions: fieldPermissionsByRoleId.get(roleEntity.id) || [], + rowLevelPermissionPredicates: + rowLevelPermissionPredicatesByRoleId.get(roleEntity.id) || [], + rowLevelPermissionPredicateGroups: + rowLevelPermissionPredicateGroupsByRoleId.get(roleEntity.id) || [], } as RoleEntity); addFlatEntityToFlatEntityMapsThroughMutationOrThrow({ diff --git a/packages/twenty-server/src/engine/metadata-modules/role/services/workspace-roles-permissions-cache.service.ts b/packages/twenty-server/src/engine/metadata-modules/role/services/workspace-roles-permissions-cache.service.ts index bd8f3b624f..b8e54e422f 100644 --- a/packages/twenty-server/src/engine/metadata-modules/role/services/workspace-roles-permissions-cache.service.ts +++ b/packages/twenty-server/src/engine/metadata-modules/role/services/workspace-roles-permissions-cache.service.ts @@ -1,6 +1,7 @@ import { Injectable } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; +import { PermissionFlagType } from 'twenty-shared/constants'; import { STANDARD_OBJECT_IDS } from 'twenty-shared/metadata'; import { type ObjectsPermissions, @@ -9,7 +10,6 @@ import { } from 'twenty-shared/types'; import { isDefined } from 'twenty-shared/utils'; import { Repository } from 'typeorm'; -import { PermissionFlagType } from 'twenty-shared/constants'; import { WorkspaceCacheProvider } from 'src/engine/workspace-cache/interfaces/workspace-cache-provider.service'; @@ -42,7 +42,13 @@ export class WorkspaceRolesPermissionsCacheService extends WorkspaceCacheProvide where: { workspaceId, }, - relations: ['objectPermissions', 'permissionFlags', 'fieldPermissions'], + relations: [ + 'objectPermissions', + 'permissionFlags', + 'fieldPermissions', + 'rowLevelPermissionPredicates', + 'rowLevelPermissionPredicateGroups', + ], }); const workspaceObjectMetadataCollection = @@ -132,6 +138,18 @@ export class WorkspaceRolesPermissionsCacheService extends WorkspaceCacheProvide canSoftDeleteObjectRecords: canSoftDelete, canDestroyObjectRecords: canDestroy, restrictedFields, + rowLevelPermissionPredicates: + role.rowLevelPermissionPredicates.filter( + (rowLevelPermissionPredicate) => + rowLevelPermissionPredicate.objectMetadataId === + objectMetadataId, + ), + rowLevelPermissionPredicateGroups: + role.rowLevelPermissionPredicateGroups.filter( + (rowLevelPermissionPredicateGroup) => + rowLevelPermissionPredicateGroup.objectMetadataId === + objectMetadataId, + ), }; } diff --git a/packages/twenty-server/src/engine/metadata-modules/role/utils/fromUserWorkspacePermissionsToUserWorkspacePermissionsDto.ts b/packages/twenty-server/src/engine/metadata-modules/role/utils/fromUserWorkspacePermissionsToUserWorkspacePermissionsDto.ts index 44b8772a87..903ed41937 100644 --- a/packages/twenty-server/src/engine/metadata-modules/role/utils/fromUserWorkspacePermissionsToUserWorkspacePermissionsDto.ts +++ b/packages/twenty-server/src/engine/metadata-modules/role/utils/fromUserWorkspacePermissionsToUserWorkspacePermissionsDto.ts @@ -15,6 +15,9 @@ export const fromUserWorkspacePermissionsToUserWorkspacePermissionsDto = ({ canSoftDeleteObjectRecords: permissions.canSoftDeleteObjectRecords, canDestroyObjectRecords: permissions.canDestroyObjectRecords, restrictedFields: permissions.restrictedFields, + rowLevelPermissionPredicates: permissions.rowLevelPermissionPredicates, + rowLevelPermissionPredicateGroups: + permissions.rowLevelPermissionPredicateGroups, }), ); diff --git a/packages/twenty-server/src/engine/metadata-modules/row-level-permission-predicate/dtos/inputs/create-row-level-permission-predicate-group.input.ts b/packages/twenty-server/src/engine/metadata-modules/row-level-permission-predicate/dtos/inputs/create-row-level-permission-predicate-group.input.ts index 8faa033d95..35ed6fae0e 100644 --- a/packages/twenty-server/src/engine/metadata-modules/row-level-permission-predicate/dtos/inputs/create-row-level-permission-predicate-group.input.ts +++ b/packages/twenty-server/src/engine/metadata-modules/row-level-permission-predicate/dtos/inputs/create-row-level-permission-predicate-group.input.ts @@ -2,7 +2,7 @@ import { Field, InputType } from '@nestjs/graphql'; -import { RowLevelPermissionPredicateGroupLogicalOperator } from 'src/engine/metadata-modules/row-level-permission-predicate/enums/row-level-permission-predicate-group-logical-operator.enum'; +import { RowLevelPermissionPredicateGroupLogicalOperator } from 'twenty-shared/types'; @InputType() export class CreateRowLevelPermissionPredicateGroupInput { diff --git a/packages/twenty-server/src/engine/metadata-modules/row-level-permission-predicate/dtos/inputs/create-row-level-permission-predicate.input.ts b/packages/twenty-server/src/engine/metadata-modules/row-level-permission-predicate/dtos/inputs/create-row-level-permission-predicate.input.ts index eca0c282cf..dba5587bc4 100644 --- a/packages/twenty-server/src/engine/metadata-modules/row-level-permission-predicate/dtos/inputs/create-row-level-permission-predicate.input.ts +++ b/packages/twenty-server/src/engine/metadata-modules/row-level-permission-predicate/dtos/inputs/create-row-level-permission-predicate.input.ts @@ -3,9 +3,10 @@ import { Field, InputType } from '@nestjs/graphql'; import GraphQLJSON from 'graphql-type-json'; - -import { RowLevelPermissionPredicateOperand } from 'src/engine/metadata-modules/row-level-permission-predicate/enums/row-level-permission-predicate-operand'; -import { RowLevelPermissionPredicateValue } from 'src/engine/metadata-modules/row-level-permission-predicate/types/row-level-permission-predicate-value.type'; +import { + RowLevelPermissionPredicateOperand, + RowLevelPermissionPredicateValue, +} from 'twenty-shared/types'; @InputType() export class CreateRowLevelPermissionPredicateInput { diff --git a/packages/twenty-server/src/engine/metadata-modules/row-level-permission-predicate/dtos/inputs/update-row-level-permission-predicate-group.input.ts b/packages/twenty-server/src/engine/metadata-modules/row-level-permission-predicate/dtos/inputs/update-row-level-permission-predicate-group.input.ts index a71608cf0e..c4f3d01b15 100644 --- a/packages/twenty-server/src/engine/metadata-modules/row-level-permission-predicate/dtos/inputs/update-row-level-permission-predicate-group.input.ts +++ b/packages/twenty-server/src/engine/metadata-modules/row-level-permission-predicate/dtos/inputs/update-row-level-permission-predicate-group.input.ts @@ -2,7 +2,7 @@ import { Field, InputType } from '@nestjs/graphql'; -import { RowLevelPermissionPredicateGroupLogicalOperator } from 'src/engine/metadata-modules/row-level-permission-predicate/enums/row-level-permission-predicate-group-logical-operator.enum'; +import { RowLevelPermissionPredicateGroupLogicalOperator } from 'twenty-shared/types'; @InputType() export class UpdateRowLevelPermissionPredicateGroupInput { diff --git a/packages/twenty-server/src/engine/metadata-modules/row-level-permission-predicate/dtos/inputs/update-row-level-permission-predicate.input.ts b/packages/twenty-server/src/engine/metadata-modules/row-level-permission-predicate/dtos/inputs/update-row-level-permission-predicate.input.ts index b5257ec658..471457d77b 100644 --- a/packages/twenty-server/src/engine/metadata-modules/row-level-permission-predicate/dtos/inputs/update-row-level-permission-predicate.input.ts +++ b/packages/twenty-server/src/engine/metadata-modules/row-level-permission-predicate/dtos/inputs/update-row-level-permission-predicate.input.ts @@ -3,9 +3,10 @@ import { Field, InputType } from '@nestjs/graphql'; import GraphQLJSON from 'graphql-type-json'; - -import { RowLevelPermissionPredicateOperand } from 'src/engine/metadata-modules/row-level-permission-predicate/enums/row-level-permission-predicate-operand'; -import { RowLevelPermissionPredicateValue } from 'src/engine/metadata-modules/row-level-permission-predicate/types/row-level-permission-predicate-value.type'; +import { + RowLevelPermissionPredicateOperand, + RowLevelPermissionPredicateValue, +} from 'twenty-shared/types'; @InputType() export class UpdateRowLevelPermissionPredicateInput { diff --git a/packages/twenty-server/src/engine/metadata-modules/row-level-permission-predicate/dtos/inputs/upsert-row-level-permission-predicates.input.ts b/packages/twenty-server/src/engine/metadata-modules/row-level-permission-predicate/dtos/inputs/upsert-row-level-permission-predicates.input.ts index 6cf53108c9..e5c80ac10a 100644 --- a/packages/twenty-server/src/engine/metadata-modules/row-level-permission-predicate/dtos/inputs/upsert-row-level-permission-predicates.input.ts +++ b/packages/twenty-server/src/engine/metadata-modules/row-level-permission-predicate/dtos/inputs/upsert-row-level-permission-predicates.input.ts @@ -11,11 +11,13 @@ import { ValidateNested, } from 'class-validator'; import GraphQLJSON from 'graphql-type-json'; +import { + RowLevelPermissionPredicateGroupLogicalOperator, + RowLevelPermissionPredicateOperand, + RowLevelPermissionPredicateValue, +} from 'twenty-shared/types'; import { UUIDScalarType } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars'; -import { RowLevelPermissionPredicateGroupLogicalOperator } from 'src/engine/metadata-modules/row-level-permission-predicate/enums/row-level-permission-predicate-group-logical-operator.enum'; -import { RowLevelPermissionPredicateOperand } from 'src/engine/metadata-modules/row-level-permission-predicate/enums/row-level-permission-predicate-operand'; -import { RowLevelPermissionPredicateValue } from 'src/engine/metadata-modules/row-level-permission-predicate/types/row-level-permission-predicate-value.type'; @InputType() export class RowLevelPermissionPredicateInput { diff --git a/packages/twenty-server/src/engine/metadata-modules/row-level-permission-predicate/dtos/row-level-permission-predicate-group.dto.ts b/packages/twenty-server/src/engine/metadata-modules/row-level-permission-predicate/dtos/row-level-permission-predicate-group.dto.ts index 719c46a51b..5db6d8467b 100644 --- a/packages/twenty-server/src/engine/metadata-modules/row-level-permission-predicate/dtos/row-level-permission-predicate-group.dto.ts +++ b/packages/twenty-server/src/engine/metadata-modules/row-level-permission-predicate/dtos/row-level-permission-predicate-group.dto.ts @@ -2,7 +2,7 @@ import { Field, ObjectType, registerEnumType } from '@nestjs/graphql'; -import { RowLevelPermissionPredicateGroupLogicalOperator } from 'src/engine/metadata-modules/row-level-permission-predicate/enums/row-level-permission-predicate-group-logical-operator.enum'; +import { RowLevelPermissionPredicateGroupLogicalOperator } from 'twenty-shared/types'; registerEnumType(RowLevelPermissionPredicateGroupLogicalOperator, { name: 'RowLevelPermissionPredicateGroupLogicalOperator', diff --git a/packages/twenty-server/src/engine/metadata-modules/row-level-permission-predicate/dtos/row-level-permission-predicate.dto.ts b/packages/twenty-server/src/engine/metadata-modules/row-level-permission-predicate/dtos/row-level-permission-predicate.dto.ts index 9ebdc5c8b9..5dedc14f18 100644 --- a/packages/twenty-server/src/engine/metadata-modules/row-level-permission-predicate/dtos/row-level-permission-predicate.dto.ts +++ b/packages/twenty-server/src/engine/metadata-modules/row-level-permission-predicate/dtos/row-level-permission-predicate.dto.ts @@ -3,9 +3,10 @@ import { Field, ObjectType, registerEnumType } from '@nestjs/graphql'; import GraphQLJSON from 'graphql-type-json'; - -import { RowLevelPermissionPredicateOperand } from 'src/engine/metadata-modules/row-level-permission-predicate/enums/row-level-permission-predicate-operand'; -import { RowLevelPermissionPredicateValue } from 'src/engine/metadata-modules/row-level-permission-predicate/types/row-level-permission-predicate-value.type'; +import { + RowLevelPermissionPredicateOperand, + RowLevelPermissionPredicateValue, +} from 'twenty-shared/types'; registerEnumType(RowLevelPermissionPredicateOperand, { name: 'RowLevelPermissionPredicateOperand', diff --git a/packages/twenty-server/src/engine/metadata-modules/row-level-permission-predicate/entities/row-level-permission-predicate-group.entity.ts b/packages/twenty-server/src/engine/metadata-modules/row-level-permission-predicate/entities/row-level-permission-predicate-group.entity.ts index 9c2650fe77..c1e812b619 100644 --- a/packages/twenty-server/src/engine/metadata-modules/row-level-permission-predicate/entities/row-level-permission-predicate-group.entity.ts +++ b/packages/twenty-server/src/engine/metadata-modules/row-level-permission-predicate/entities/row-level-permission-predicate-group.entity.ts @@ -1,5 +1,9 @@ /* @license Enterprise */ +import { + RowLevelPermissionPredicateGroup, + RowLevelPermissionPredicateGroupLogicalOperator, +} from 'twenty-shared/types'; import { Column, CreateDateColumn, @@ -17,7 +21,6 @@ import { import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity'; import { RoleEntity } from 'src/engine/metadata-modules/role/role.entity'; import { RowLevelPermissionPredicateEntity } from 'src/engine/metadata-modules/row-level-permission-predicate/entities/row-level-permission-predicate.entity'; -import { RowLevelPermissionPredicateGroupLogicalOperator } from 'src/engine/metadata-modules/row-level-permission-predicate/enums/row-level-permission-predicate-group-logical-operator.enum'; import { SyncableEntity } from 'src/engine/workspace-manager/types/syncable-entity.interface'; @Entity({ name: 'rowLevelPermissionPredicateGroup', schema: 'core' }) @@ -28,7 +31,9 @@ import { SyncableEntity } from 'src/engine/workspace-manager/types/syncable-enti ]) export class RowLevelPermissionPredicateGroupEntity extends SyncableEntity - implements Required + implements + Required, + RowLevelPermissionPredicateGroup { @PrimaryGeneratedColumn('uuid') id: string; diff --git a/packages/twenty-server/src/engine/metadata-modules/row-level-permission-predicate/entities/row-level-permission-predicate.entity.ts b/packages/twenty-server/src/engine/metadata-modules/row-level-permission-predicate/entities/row-level-permission-predicate.entity.ts index a8e11bdad4..56413fc080 100644 --- a/packages/twenty-server/src/engine/metadata-modules/row-level-permission-predicate/entities/row-level-permission-predicate.entity.ts +++ b/packages/twenty-server/src/engine/metadata-modules/row-level-permission-predicate/entities/row-level-permission-predicate.entity.ts @@ -1,5 +1,10 @@ /* @license Enterprise */ +import { + RowLevelPermissionPredicate, + RowLevelPermissionPredicateOperand, + RowLevelPermissionPredicateValue, +} from 'twenty-shared/types'; import { Column, CreateDateColumn, @@ -17,8 +22,6 @@ import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/ import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity'; import { RoleEntity } from 'src/engine/metadata-modules/role/role.entity'; import { RowLevelPermissionPredicateGroupEntity } from 'src/engine/metadata-modules/row-level-permission-predicate/entities/row-level-permission-predicate-group.entity'; -import { RowLevelPermissionPredicateOperand } from 'src/engine/metadata-modules/row-level-permission-predicate/enums/row-level-permission-predicate-operand'; -import { type RowLevelPermissionPredicateValue } from 'src/engine/metadata-modules/row-level-permission-predicate/types/row-level-permission-predicate-value.type'; import { SyncableEntity } from 'src/engine/workspace-manager/types/syncable-entity.interface'; @Entity({ name: 'rowLevelPermissionPredicate', schema: 'core' }) @@ -31,7 +34,9 @@ import { SyncableEntity } from 'src/engine/workspace-manager/types/syncable-enti @Index('IDX_RLPP_GROUP_ID', ['rowLevelPermissionPredicateGroupId']) export class RowLevelPermissionPredicateEntity extends SyncableEntity - implements Required + implements + Required, + RowLevelPermissionPredicate { @PrimaryGeneratedColumn('uuid') id: string; diff --git a/packages/twenty-server/src/engine/metadata-modules/row-level-permission-predicate/resolvers/row-level-permission-predicate-group.resolver.ts b/packages/twenty-server/src/engine/metadata-modules/row-level-permission-predicate/resolvers/row-level-permission-predicate-group.resolver.ts index 3267cc5da9..8c37326d13 100644 --- a/packages/twenty-server/src/engine/metadata-modules/row-level-permission-predicate/resolvers/row-level-permission-predicate-group.resolver.ts +++ b/packages/twenty-server/src/engine/metadata-modules/row-level-permission-predicate/resolvers/row-level-permission-predicate-group.resolver.ts @@ -3,6 +3,8 @@ import { UseGuards } from '@nestjs/common'; import { Args, Mutation, Query, Resolver } from '@nestjs/graphql'; +import { RowLevelPermissionPredicateGroupLogicalOperator } from 'twenty-shared/types'; + import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity'; import { AuthWorkspace } from 'src/engine/decorators/auth/auth-workspace.decorator'; import { NoPermissionGuard } from 'src/engine/guards/no-permission.guard'; @@ -11,7 +13,6 @@ import { CreateRowLevelPermissionPredicateGroupInput } from 'src/engine/metadata import { DeleteRowLevelPermissionPredicateGroupInput } from 'src/engine/metadata-modules/row-level-permission-predicate/dtos/inputs/delete-row-level-permission-predicate-group.input'; import { UpdateRowLevelPermissionPredicateGroupInput } from 'src/engine/metadata-modules/row-level-permission-predicate/dtos/inputs/update-row-level-permission-predicate-group.input'; import { RowLevelPermissionPredicateGroupDTO } from 'src/engine/metadata-modules/row-level-permission-predicate/dtos/row-level-permission-predicate-group.dto'; -import { RowLevelPermissionPredicateGroupLogicalOperator } from 'src/engine/metadata-modules/row-level-permission-predicate/enums/row-level-permission-predicate-group-logical-operator.enum'; import { RowLevelPermissionPredicateGroupService } from 'src/engine/metadata-modules/row-level-permission-predicate/services/row-level-permission-predicate-group.service'; @Resolver(() => RowLevelPermissionPredicateGroupDTO) diff --git a/packages/twenty-server/src/engine/metadata-modules/row-level-permission-predicate/resolvers/row-level-permission-predicate.resolver.ts b/packages/twenty-server/src/engine/metadata-modules/row-level-permission-predicate/resolvers/row-level-permission-predicate.resolver.ts index a45f3de784..c4744896a3 100644 --- a/packages/twenty-server/src/engine/metadata-modules/row-level-permission-predicate/resolvers/row-level-permission-predicate.resolver.ts +++ b/packages/twenty-server/src/engine/metadata-modules/row-level-permission-predicate/resolvers/row-level-permission-predicate.resolver.ts @@ -3,6 +3,8 @@ import { UseGuards } from '@nestjs/common'; import { Args, Mutation, Query, Resolver } from '@nestjs/graphql'; +import { RowLevelPermissionPredicateOperand } from 'twenty-shared/types'; + import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity'; import { AuthWorkspace } from 'src/engine/decorators/auth/auth-workspace.decorator'; import { NoPermissionGuard } from 'src/engine/guards/no-permission.guard'; @@ -11,7 +13,6 @@ import { CreateRowLevelPermissionPredicateInput } from 'src/engine/metadata-modu import { DeleteRowLevelPermissionPredicateInput } from 'src/engine/metadata-modules/row-level-permission-predicate/dtos/inputs/delete-row-level-permission-predicate.input'; import { UpdateRowLevelPermissionPredicateInput } from 'src/engine/metadata-modules/row-level-permission-predicate/dtos/inputs/update-row-level-permission-predicate.input'; import { RowLevelPermissionPredicateDTO } from 'src/engine/metadata-modules/row-level-permission-predicate/dtos/row-level-permission-predicate.dto'; -import { RowLevelPermissionPredicateOperand } from 'src/engine/metadata-modules/row-level-permission-predicate/enums/row-level-permission-predicate-operand'; import { RowLevelPermissionPredicateService } from 'src/engine/metadata-modules/row-level-permission-predicate/services/row-level-permission-predicate.service'; @Resolver(() => RowLevelPermissionPredicateDTO) diff --git a/packages/twenty-server/src/engine/metadata-modules/row-level-permission-predicate/services/row-level-permission-predicate-group.service.ts b/packages/twenty-server/src/engine/metadata-modules/row-level-permission-predicate/services/row-level-permission-predicate-group.service.ts index cb4f172982..c533ecf66c 100644 --- a/packages/twenty-server/src/engine/metadata-modules/row-level-permission-predicate/services/row-level-permission-predicate-group.service.ts +++ b/packages/twenty-server/src/engine/metadata-modules/row-level-permission-predicate/services/row-level-permission-predicate-group.service.ts @@ -18,19 +18,16 @@ import { type DestroyRowLevelPermissionPredicateGroupInput } from 'src/engine/me import { type UpdateRowLevelPermissionPredicateGroupInput } from 'src/engine/metadata-modules/row-level-permission-predicate/dtos/inputs/update-row-level-permission-predicate-group.input'; import { RowLevelPermissionPredicateGroupDTO } from 'src/engine/metadata-modules/row-level-permission-predicate/dtos/row-level-permission-predicate-group.dto'; import { type FlatRowLevelPermissionPredicateGroup } from 'src/engine/metadata-modules/row-level-permission-predicate/types/flat-row-level-permission-predicate-group.type'; +import { WorkspaceCacheService } from 'src/engine/workspace-cache/services/workspace-cache.service'; import { WorkspaceMigrationBuilderException } from 'src/engine/workspace-manager/workspace-migration/exceptions/workspace-migration-builder-exception'; import { WorkspaceMigrationValidateBuildAndRunService } from 'src/engine/workspace-manager/workspace-migration/services/workspace-migration-validate-build-and-run-service'; -const RLP_CACHE_KEYS = [ - 'flatRowLevelPermissionPredicateMaps', - 'flatRowLevelPermissionPredicateGroupMaps', -] as const; - @Injectable() export class RowLevelPermissionPredicateGroupService { constructor( private readonly workspaceMigrationValidateBuildAndRunService: WorkspaceMigrationValidateBuildAndRunService, private readonly flatEntityMapsCacheService: WorkspaceManyOrAllFlatEntityMapsCacheService, + private readonly workspaceCacheService: WorkspaceCacheService, ) {} async createOne({ @@ -290,9 +287,9 @@ export class RowLevelPermissionPredicateGroupService { ); } - await this.flatEntityMapsCacheService.invalidateFlatEntityMaps({ - workspaceId, - flatMapsKeys: [...RLP_CACHE_KEYS], - }); + await this.workspaceCacheService.invalidateAndRecompute(workspaceId, [ + 'rolesPermissions', + 'flatRowLevelPermissionPredicateMaps', + ]); } } diff --git a/packages/twenty-server/src/engine/metadata-modules/row-level-permission-predicate/services/row-level-permission-predicate.service.ts b/packages/twenty-server/src/engine/metadata-modules/row-level-permission-predicate/services/row-level-permission-predicate.service.ts index 9810bda72e..e5c8cb2fba 100644 --- a/packages/twenty-server/src/engine/metadata-modules/row-level-permission-predicate/services/row-level-permission-predicate.service.ts +++ b/packages/twenty-server/src/engine/metadata-modules/row-level-permission-predicate/services/row-level-permission-predicate.service.ts @@ -28,6 +28,7 @@ import { RowLevelPermissionPredicateGroupDTO } from 'src/engine/metadata-modules import { RowLevelPermissionPredicateDTO } from 'src/engine/metadata-modules/row-level-permission-predicate/dtos/row-level-permission-predicate.dto'; import { type FlatRowLevelPermissionPredicateGroup } from 'src/engine/metadata-modules/row-level-permission-predicate/types/flat-row-level-permission-predicate-group.type'; import { type FlatRowLevelPermissionPredicate } from 'src/engine/metadata-modules/row-level-permission-predicate/types/flat-row-level-permission-predicate.type'; +import { WorkspaceCacheService } from 'src/engine/workspace-cache/services/workspace-cache.service'; import { WorkspaceMigrationBuilderException } from 'src/engine/workspace-manager/workspace-migration/exceptions/workspace-migration-builder-exception'; import { WorkspaceMigrationValidateBuildAndRunService } from 'src/engine/workspace-manager/workspace-migration/services/workspace-migration-validate-build-and-run-service'; @@ -36,6 +37,7 @@ export class RowLevelPermissionPredicateService { constructor( private readonly workspaceMigrationValidateBuildAndRunService: WorkspaceMigrationValidateBuildAndRunService, private readonly flatEntityMapsCacheService: WorkspaceManyOrAllFlatEntityMapsCacheService, + private readonly workspaceCacheService: WorkspaceCacheService, ) {} async createOne({ @@ -607,6 +609,10 @@ export class RowLevelPermissionPredicateService { 'Validation errors occurred while upserting row level permission predicates', ); } + + await this.workspaceCacheService.invalidateAndRecompute(workspaceId, [ + 'rolesPermissions', + ]); } private async runMigration({ @@ -641,5 +647,10 @@ export class RowLevelPermissionPredicateService { 'Validation errors occurred while applying row level permission predicate mutation', ); } + + await this.workspaceCacheService.invalidateAndRecompute(workspaceId, [ + 'rolesPermissions', + 'flatRowLevelPermissionPredicateGroupMaps', + ]); } } diff --git a/packages/twenty-server/src/engine/twenty-orm/entity-manager/workspace-entity-manager.spec.ts b/packages/twenty-server/src/engine/twenty-orm/entity-manager/workspace-entity-manager.spec.ts index 6715c0e28e..6a0d73ec48 100644 --- a/packages/twenty-server/src/engine/twenty-orm/entity-manager/workspace-entity-manager.spec.ts +++ b/packages/twenty-server/src/engine/twenty-orm/entity-manager/workspace-entity-manager.spec.ts @@ -257,6 +257,8 @@ describe('WorkspaceEntityManager', () => { canSoftDeleteObjectRecords: false, canDestroyObjectRecords: false, restrictedFields: {}, + rowLevelPermissionPredicates: [], + rowLevelPermissionPredicateGroups: [], }, }, }; diff --git a/packages/twenty-server/src/engine/twenty-orm/exceptions/twenty-orm.exception.ts b/packages/twenty-server/src/engine/twenty-orm/exceptions/twenty-orm.exception.ts index ede2e90247..95dd81e3e3 100644 --- a/packages/twenty-server/src/engine/twenty-orm/exceptions/twenty-orm.exception.ts +++ b/packages/twenty-server/src/engine/twenty-orm/exceptions/twenty-orm.exception.ts @@ -25,6 +25,8 @@ export enum TwentyORMExceptionCode { TOO_MANY_RECORDS_TO_UPDATE = 'TOO_MANY_RECORDS_TO_UPDATE', INVALID_INPUT = 'INVALID_INPUT', ORM_EVENT_DATA_CORRUPTED = 'ORM_EVENT_DATA_CORRUPTED', + RLS_VALIDATION_FAILED = 'RLS_VALIDATION_FAILED', + NO_ROLE_FOUND_FOR_USER_WORKSPACE = 'NO_ROLE_FOUND_FOR_USER_WORKSPACE', } const getTwentyORMExceptionUserFriendlyMessage = ( @@ -65,9 +67,13 @@ const getTwentyORMExceptionUserFriendlyMessage = ( return msg`Too many records to update at once.`; case TwentyORMExceptionCode.INVALID_INPUT: return msg`Invalid input provided.`; + case TwentyORMExceptionCode.RLS_VALIDATION_FAILED: + return msg`Record does not satisfy security constraints.`; case TwentyORMExceptionCode.ENUM_TYPE_NAME_NOT_FOUND: case TwentyORMExceptionCode.ORM_EVENT_DATA_CORRUPTED: return STANDARD_ERROR_MESSAGE; + case TwentyORMExceptionCode.NO_ROLE_FOUND_FOR_USER_WORKSPACE: + return msg`No role found for user.`; default: assertUnreachable(code); } diff --git a/packages/twenty-server/src/engine/twenty-orm/repository/__tests__/workspace.repository.spec.ts b/packages/twenty-server/src/engine/twenty-orm/repository/__tests__/workspace.repository.spec.ts index ad10ac832b..6f735126a7 100644 --- a/packages/twenty-server/src/engine/twenty-orm/repository/__tests__/workspace.repository.spec.ts +++ b/packages/twenty-server/src/engine/twenty-orm/repository/__tests__/workspace.repository.spec.ts @@ -145,6 +145,8 @@ describe('WorkspaceRepository', () => { canSoftDeleteObjectRecords: false, canDestroyObjectRecords: false, restrictedFields: {}, + rowLevelPermissionPredicates: [], + rowLevelPermissionPredicateGroups: [], }, }; mockQueryRunner = {} as QueryRunner; diff --git a/packages/twenty-server/src/engine/twenty-orm/repository/workspace-insert-query-builder.ts b/packages/twenty-server/src/engine/twenty-orm/repository/workspace-insert-query-builder.ts index 4968334f35..f9cde8c9e9 100644 --- a/packages/twenty-server/src/engine/twenty-orm/repository/workspace-insert-query-builder.ts +++ b/packages/twenty-server/src/engine/twenty-orm/repository/workspace-insert-query-builder.ts @@ -31,6 +31,7 @@ import { formatData } from 'src/engine/twenty-orm/utils/format-data.util'; import { formatResult } from 'src/engine/twenty-orm/utils/format-result.util'; import { formatTwentyOrmEventToDatabaseBatchEvent } from 'src/engine/twenty-orm/utils/format-twenty-orm-event-to-database-batch-event.util'; import { getObjectMetadataFromEntityTarget } from 'src/engine/twenty-orm/utils/get-object-metadata-from-entity-target.util'; +import { validateRLSPredicatesForRecords } from 'src/engine/twenty-orm/utils/validate-rls-predicates-for-records.util'; export class WorkspaceInsertQueryBuilder< T extends ObjectLiteral, @@ -178,6 +179,8 @@ export class WorkspaceInsertQueryBuilder< this.expressionMap.valuesSet = updatedValues; } + this.validateRLSPredicatesForInsert(); + const result = await super.execute(); const eventSelectQueryBuilder = ( this.connection.manager as WorkspaceEntityManager @@ -269,6 +272,26 @@ export class WorkspaceInsertQueryBuilder< } } + private validateRLSPredicatesForInsert(): void { + const mainAliasTarget = this.getMainAliasTarget(); + const objectMetadata = getObjectMetadataFromEntityTarget( + mainAliasTarget, + this.internalContext, + ); + + const valuesToInsert = Array.isArray(this.expressionMap.valuesSet) + ? this.expressionMap.valuesSet + : [this.expressionMap.valuesSet]; + + validateRLSPredicatesForRecords({ + records: valuesToInsert, + objectMetadata, + internalContext: this.internalContext, + authContext: this.authContext, + shouldBypassPermissionChecks: this.shouldBypassPermissionChecks, + }); + } + private getMainAliasTarget(): EntityTarget { const mainAliasTarget = this.expressionMap.mainAlias?.target; diff --git a/packages/twenty-server/src/engine/twenty-orm/repository/workspace-update-query-builder.ts b/packages/twenty-server/src/engine/twenty-orm/repository/workspace-update-query-builder.ts index 9b1e335fb5..615bf864cf 100644 --- a/packages/twenty-server/src/engine/twenty-orm/repository/workspace-update-query-builder.ts +++ b/packages/twenty-server/src/engine/twenty-orm/repository/workspace-update-query-builder.ts @@ -37,6 +37,7 @@ import { formatData } from 'src/engine/twenty-orm/utils/format-data.util'; import { formatResult } from 'src/engine/twenty-orm/utils/format-result.util'; import { formatTwentyOrmEventToDatabaseBatchEvent } from 'src/engine/twenty-orm/utils/format-twenty-orm-event-to-database-batch-event.util'; import { getObjectMetadataFromEntityTarget } from 'src/engine/twenty-orm/utils/get-object-metadata-from-entity-target.util'; +import { validateRLSPredicatesForRecords } from 'src/engine/twenty-orm/utils/validate-rls-predicates-for-records.util'; import { computeTableName } from 'src/engine/utils/compute-table-name.util'; export class WorkspaceUpdateQueryBuilder< @@ -175,6 +176,21 @@ export class WorkspaceUpdateQueryBuilder< this.applyRowLevelPermissionPredicates(); + const valuesSet = this.expressionMap.valuesSet ?? {}; + const updatedRecords: T[] = formattedBefore.map( + (record, index) => + ({ + ...record, + ...(Array.isArray(valuesSet) + ? (valuesSet[index] ?? valuesSet[0] ?? {}) + : valuesSet), + }) as T, + ); + + this.validateRLSPredicatesForUpdate({ + updatedRecords, + }); + const result = await super.execute(); const after = await eventSelectQueryBuilder.getMany(); @@ -323,12 +339,34 @@ export class WorkspaceUpdateQueryBuilder< })); } + const beforeRecordById = new Map(); + + for (const beforeRecord of formattedBefore) { + if (isDefined(beforeRecord.id)) { + beforeRecordById.set(beforeRecord.id, beforeRecord); + } + } + for (const input of this.manyInputs) { this.expressionMap.valuesSet = input.partialEntity; this.where({ id: input.criteria }); this.applyRowLevelPermissionPredicates(); + const beforeRecord = beforeRecordById.get(input.criteria); + const updatedRecords = beforeRecord + ? [ + { + ...beforeRecord, + ...input.partialEntity, + } as T, + ] + : []; + + this.validateRLSPredicatesForUpdate({ + updatedRecords, + }); + const result = await super.execute(); results.push(result); @@ -521,4 +559,26 @@ export class WorkspaceUpdateQueryBuilder< featureFlagMap: this.featureFlagMap, }); } + + private validateRLSPredicatesForUpdate({ + updatedRecords, + }: { + updatedRecords: T[]; + }): void { + const mainAliasTarget = this.getMainAliasTarget(); + const objectMetadata = getObjectMetadataFromEntityTarget( + mainAliasTarget, + this.internalContext, + ); + + validateRLSPredicatesForRecords({ + records: updatedRecords, + objectMetadata, + internalContext: this.internalContext, + authContext: this.authContext, + shouldBypassPermissionChecks: this.shouldBypassPermissionChecks, + errorMessage: + 'Updated record does not satisfy row-level security constraints of your current role', + }); + } } diff --git a/packages/twenty-server/src/engine/twenty-orm/utils/__tests__/compute-permission-intersection.util.spec.ts b/packages/twenty-server/src/engine/twenty-orm/utils/__tests__/compute-permission-intersection.util.spec.ts index dd5dd2c182..0695f7b64a 100644 --- a/packages/twenty-server/src/engine/twenty-orm/utils/__tests__/compute-permission-intersection.util.spec.ts +++ b/packages/twenty-server/src/engine/twenty-orm/utils/__tests__/compute-permission-intersection.util.spec.ts @@ -21,6 +21,8 @@ describe('computePermissionIntersection', () => { canSoftDeleteObjectRecords: false, canDestroyObjectRecords: false, restrictedFields: {}, + rowLevelPermissionPredicates: [], + rowLevelPermissionPredicateGroups: [], }, }; @@ -39,6 +41,8 @@ describe('computePermissionIntersection', () => { canSoftDeleteObjectRecords: true, canDestroyObjectRecords: true, restrictedFields: {}, + rowLevelPermissionPredicates: [], + rowLevelPermissionPredicateGroups: [], }, }; @@ -49,6 +53,8 @@ describe('computePermissionIntersection', () => { canSoftDeleteObjectRecords: true, canDestroyObjectRecords: true, restrictedFields: {}, + rowLevelPermissionPredicates: [], + rowLevelPermissionPredicateGroups: [], }, }; @@ -63,6 +69,8 @@ describe('computePermissionIntersection', () => { canSoftDeleteObjectRecords: true, canDestroyObjectRecords: true, restrictedFields: {}, + rowLevelPermissionPredicates: [], + rowLevelPermissionPredicateGroups: [], }); }); @@ -74,6 +82,8 @@ describe('computePermissionIntersection', () => { canSoftDeleteObjectRecords: true, canDestroyObjectRecords: true, restrictedFields: {}, + rowLevelPermissionPredicates: [], + rowLevelPermissionPredicateGroups: [], }, }; @@ -84,6 +94,8 @@ describe('computePermissionIntersection', () => { canSoftDeleteObjectRecords: false, canDestroyObjectRecords: true, restrictedFields: {}, + rowLevelPermissionPredicates: [], + rowLevelPermissionPredicateGroups: [], }, }; @@ -98,6 +110,8 @@ describe('computePermissionIntersection', () => { canSoftDeleteObjectRecords: false, canDestroyObjectRecords: true, restrictedFields: {}, + rowLevelPermissionPredicates: [], + rowLevelPermissionPredicateGroups: [], }); }); @@ -109,6 +123,8 @@ describe('computePermissionIntersection', () => { canSoftDeleteObjectRecords: true, canDestroyObjectRecords: true, restrictedFields: {}, + rowLevelPermissionPredicates: [], + rowLevelPermissionPredicateGroups: [], }, }; @@ -125,6 +141,8 @@ describe('computePermissionIntersection', () => { canSoftDeleteObjectRecords: false, canDestroyObjectRecords: false, restrictedFields: {}, + rowLevelPermissionPredicates: [], + rowLevelPermissionPredicateGroups: [], }); }); }); @@ -138,6 +156,8 @@ describe('computePermissionIntersection', () => { canSoftDeleteObjectRecords: false, canDestroyObjectRecords: false, restrictedFields: {}, + rowLevelPermissionPredicates: [], + rowLevelPermissionPredicateGroups: [], }, [objectMetadataId2]: { canReadObjectRecords: true, @@ -145,6 +165,8 @@ describe('computePermissionIntersection', () => { canSoftDeleteObjectRecords: false, canDestroyObjectRecords: false, restrictedFields: {}, + rowLevelPermissionPredicates: [], + rowLevelPermissionPredicateGroups: [], }, }; @@ -155,6 +177,8 @@ describe('computePermissionIntersection', () => { canSoftDeleteObjectRecords: true, canDestroyObjectRecords: false, restrictedFields: {}, + rowLevelPermissionPredicates: [], + rowLevelPermissionPredicateGroups: [], }, [objectMetadataId2]: { canReadObjectRecords: true, @@ -162,6 +186,8 @@ describe('computePermissionIntersection', () => { canSoftDeleteObjectRecords: false, canDestroyObjectRecords: false, restrictedFields: {}, + rowLevelPermissionPredicates: [], + rowLevelPermissionPredicateGroups: [], }, }; @@ -176,6 +202,8 @@ describe('computePermissionIntersection', () => { canSoftDeleteObjectRecords: false, canDestroyObjectRecords: false, restrictedFields: {}, + rowLevelPermissionPredicates: [], + rowLevelPermissionPredicateGroups: [], }); expect(result[objectMetadataId2]).toEqual({ @@ -184,6 +212,8 @@ describe('computePermissionIntersection', () => { canSoftDeleteObjectRecords: false, canDestroyObjectRecords: false, restrictedFields: {}, + rowLevelPermissionPredicates: [], + rowLevelPermissionPredicateGroups: [], }); }); }); @@ -206,6 +236,8 @@ describe('computePermissionIntersection', () => { canUpdate: null, }, }, + rowLevelPermissionPredicates: [], + rowLevelPermissionPredicateGroups: [], }, }; @@ -225,6 +257,8 @@ describe('computePermissionIntersection', () => { canUpdate: false, }, }, + rowLevelPermissionPredicates: [], + rowLevelPermissionPredicateGroups: [], }, }; @@ -258,6 +292,8 @@ describe('computePermissionIntersection', () => { canUpdate: false, }, }, + rowLevelPermissionPredicates: [], + rowLevelPermissionPredicateGroups: [], }, }; @@ -273,6 +309,8 @@ describe('computePermissionIntersection', () => { canUpdate: false, }, }, + rowLevelPermissionPredicates: [], + rowLevelPermissionPredicateGroups: [], }, }; @@ -303,6 +341,8 @@ describe('computePermissionIntersection', () => { canSoftDeleteObjectRecords: true, canDestroyObjectRecords: true, restrictedFields: {}, + rowLevelPermissionPredicates: [], + rowLevelPermissionPredicateGroups: [], }, }; @@ -313,6 +353,8 @@ describe('computePermissionIntersection', () => { canSoftDeleteObjectRecords: false, canDestroyObjectRecords: true, restrictedFields: {}, + rowLevelPermissionPredicates: [], + rowLevelPermissionPredicateGroups: [], }, }; @@ -323,6 +365,8 @@ describe('computePermissionIntersection', () => { canSoftDeleteObjectRecords: true, canDestroyObjectRecords: true, restrictedFields: {}, + rowLevelPermissionPredicates: [], + rowLevelPermissionPredicateGroups: [], }, }; @@ -334,6 +378,8 @@ describe('computePermissionIntersection', () => { canSoftDeleteObjectRecords: false, canDestroyObjectRecords: true, restrictedFields: {}, + rowLevelPermissionPredicates: [], + rowLevelPermissionPredicateGroups: [], }); }); }); diff --git a/packages/twenty-server/src/engine/twenty-orm/utils/build-row-level-permission-record-filter.util.ts b/packages/twenty-server/src/engine/twenty-orm/utils/build-row-level-permission-record-filter.util.ts index 07e26b47b6..ed37a58484 100644 --- a/packages/twenty-server/src/engine/twenty-orm/utils/build-row-level-permission-record-filter.util.ts +++ b/packages/twenty-server/src/engine/twenty-orm/utils/build-row-level-permission-record-filter.util.ts @@ -3,9 +3,11 @@ import { FieldMetadataType, RecordFilterGroupLogicalOperator, + RowLevelPermissionPredicateGroupLogicalOperator, type CompositeFieldSubFieldName, type PartialFieldMetadataItemOption, type RecordGqlOperationFilter, + type RowLevelPermissionPredicateValue, } from 'twenty-shared/types'; import { computeRecordGqlOperationFilter, @@ -25,10 +27,8 @@ import { PermissionsException, PermissionsExceptionCode, } from 'src/engine/metadata-modules/permissions/permissions.exception'; -import { RowLevelPermissionPredicateGroupLogicalOperator } from 'src/engine/metadata-modules/row-level-permission-predicate/enums/row-level-permission-predicate-group-logical-operator.enum'; import { type FlatRowLevelPermissionPredicateGroupMaps } from 'src/engine/metadata-modules/row-level-permission-predicate/types/flat-row-level-permission-predicate-group-maps.type'; import { type FlatRowLevelPermissionPredicateMaps } from 'src/engine/metadata-modules/row-level-permission-predicate/types/flat-row-level-permission-predicate-maps.type'; -import { type RowLevelPermissionPredicateValue } from 'src/engine/metadata-modules/row-level-permission-predicate/types/row-level-permission-predicate-value.type'; import { validateEnumValueCompatibility } from 'src/engine/twenty-orm/utils/validate-enum-value-compatibility.util'; type BuildRowLevelPermissionRecordFilterArgs = { diff --git a/packages/twenty-server/src/engine/twenty-orm/utils/compute-permission-intersection.util.ts b/packages/twenty-server/src/engine/twenty-orm/utils/compute-permission-intersection.util.ts index f6ea3c373b..655f074f44 100644 --- a/packages/twenty-server/src/engine/twenty-orm/utils/compute-permission-intersection.util.ts +++ b/packages/twenty-server/src/engine/twenty-orm/utils/compute-permission-intersection.util.ts @@ -85,6 +85,8 @@ export const computePermissionIntersection = ( canSoftDeleteObjectRecords, canDestroyObjectRecords, restrictedFields, + rowLevelPermissionPredicates: [], + rowLevelPermissionPredicateGroups: [], }; } diff --git a/packages/twenty-server/src/engine/twenty-orm/utils/is-record-matching-rls-row-level-permission-predicate.util.ts b/packages/twenty-server/src/engine/twenty-orm/utils/is-record-matching-rls-row-level-permission-predicate.util.ts new file mode 100644 index 0000000000..a9dc73506d --- /dev/null +++ b/packages/twenty-server/src/engine/twenty-orm/utils/is-record-matching-rls-row-level-permission-predicate.util.ts @@ -0,0 +1,423 @@ +/* @license Enterprise */ + +import { isObject } from '@sniptt/guards'; +import { + FieldMetadataType, + type ActorFilter, + type AddressFilter, + type AndObjectRecordFilter, + type ArrayFilter, + type BooleanFilter, + type CurrencyFilter, + type DateFilter, + type EmailsFilter, + type FloatFilter, + type FullNameFilter, + type LeafObjectRecordFilter, + type LinksFilter, + type MultiSelectFilter, + type NotObjectRecordFilter, + type OrObjectRecordFilter, + type PhonesFilter, + type RatingFilter, + type RawJsonFilter, + type RecordGqlOperationFilter, + type RichTextV2Filter, + type SelectFilter, + type StringFilter, + type TSVectorFilter, + type UUIDFilter, +} from 'twenty-shared/types'; +import { + isDefined, + isEmptyObject, + isMatchingArrayFilter, + isMatchingBooleanFilter, + isMatchingCurrencyFilter, + isMatchingDateFilter, + isMatchingFloatFilter, + isMatchingMultiSelectFilter, + isMatchingRatingFilter, + isMatchingRawJsonFilter, + isMatchingRichTextV2Filter, + isMatchingSelectFilter, + isMatchingStringFilter, + isMatchingTSVectorFilter, + isMatchingUUIDFilter, +} from 'twenty-shared/utils'; + +import { getFlatFieldsFromFlatObjectMetadata } from 'src/engine/api/graphql/workspace-schema-builder/utils/get-flat-fields-for-flat-object-metadata.util'; +import { type FlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-maps.type'; +import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type'; +import { type FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type'; + +const isLeafFilter = ( + filter: RecordGqlOperationFilter, +): filter is LeafObjectRecordFilter => { + return !isAndFilter(filter) && !isOrFilter(filter) && !isNotFilter(filter); +}; + +const isAndFilter = ( + filter: RecordGqlOperationFilter, +): filter is AndObjectRecordFilter => 'and' in filter && !!filter.and; + +const isImplicitAndFilter = (filter: RecordGqlOperationFilter) => + Object.keys(filter).length > 1; + +const isOrFilter = ( + filter: RecordGqlOperationFilter, +): filter is OrObjectRecordFilter => 'or' in filter && !!filter.or; + +const isNotFilter = ( + filter: RecordGqlOperationFilter, +): filter is NotObjectRecordFilter => 'not' in filter && !!filter.not; + +export const isRecordMatchingRLSRowLevelPermissionPredicate = ({ + record, + filter, + flatObjectMetadata, + flatFieldMetadataMaps, +}: { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + record: any; + filter: RecordGqlOperationFilter; + flatObjectMetadata: FlatObjectMetadata; + flatFieldMetadataMaps: FlatEntityMaps; +}): boolean => { + if (Object.keys(filter).length === 0 && record.deletedAt === null) { + return true; + } + + if (isImplicitAndFilter(filter)) { + return Object.entries(filter).every(([filterKey, value]) => + isRecordMatchingRLSRowLevelPermissionPredicate({ + record, + filter: { [filterKey]: value }, + flatObjectMetadata, + flatFieldMetadataMaps, + }), + ); + } + + if (isAndFilter(filter)) { + const filterValue = filter.and; + + if (!Array.isArray(filterValue)) { + throw new Error( + 'Unexpected value for "and" filter : ' + JSON.stringify(filterValue), + ); + } + + return ( + filterValue.length === 0 || + filterValue.every((andFilter) => + isRecordMatchingRLSRowLevelPermissionPredicate({ + record, + filter: andFilter, + flatObjectMetadata, + flatFieldMetadataMaps, + }), + ) + ); + } + + if (isOrFilter(filter)) { + const filterValue = filter.or; + + if (Array.isArray(filterValue)) { + return ( + filterValue.length === 0 || + filterValue.some((orFilter) => + isRecordMatchingRLSRowLevelPermissionPredicate({ + record, + filter: orFilter, + flatObjectMetadata, + flatFieldMetadataMaps, + }), + ) + ); + } + + if (isObject(filterValue)) { + // The API considers "or" with an object as an "and" + return isRecordMatchingRLSRowLevelPermissionPredicate({ + record, + filter: filterValue, + flatObjectMetadata, + flatFieldMetadataMaps, + }); + } + + throw new Error('Unexpected value for "or" filter : ' + filterValue); + } + + if (isNotFilter(filter)) { + const filterValue = filter.not; + + if (!isDefined(filterValue)) { + throw new Error('Unexpected value for "not" filter : ' + filterValue); + } + + return ( + isEmptyObject(filterValue) || + !isRecordMatchingRLSRowLevelPermissionPredicate({ + record, + filter: filterValue, + flatObjectMetadata, + flatFieldMetadataMaps, + }) + ); + } + + if (isLeafFilter(filter)) { + if (isDefined(record.deletedAt) && filter.deletedAt === undefined) { + return false; + } + } + + const objectFields = getFlatFieldsFromFlatObjectMetadata( + flatObjectMetadata, + flatFieldMetadataMaps, + ); + + return Object.entries(filter).every(([filterKey, filterValue]) => { + if (!isDefined(filterValue)) { + throw new Error( + 'Unexpected value for filter key "' + filterKey + '" : ' + filterValue, + ); + } + + if (isEmptyObject(filterValue)) return true; + + const objectMetadataField = + objectFields.find((field) => field.name === filterKey) ?? + objectFields.find( + (field) => + field.type === FieldMetadataType.RELATION && + (field.settings as { joinColumnName?: string } | undefined) + ?.joinColumnName === filterKey, + ); + + if (!isDefined(objectMetadataField)) { + throw new Error( + 'Field metadata item "' + + filterKey + + '" not found for object metadata item ' + + flatObjectMetadata.nameSingular, + ); + } + + switch (objectMetadataField.type) { + case FieldMetadataType.RATING: + return isMatchingRatingFilter({ + ratingFilter: filterValue as RatingFilter, + value: record[filterKey], + }); + case FieldMetadataType.TEXT: { + return isMatchingStringFilter({ + stringFilter: filterValue as StringFilter, + value: record[filterKey], + }); + } + case FieldMetadataType.RICH_TEXT: { + // TODO: Implement a better rich text filter once it becomes a composite field + // See this issue for more context: https://github.com/twentyhq/twenty/issues/7613#issuecomment-2408944585 + // This should be tackled in Q4'24 + return isMatchingStringFilter({ + stringFilter: filterValue as StringFilter, + value: record[filterKey], + }); + } + case FieldMetadataType.RICH_TEXT_V2: { + return isMatchingRichTextV2Filter({ + richTextV2Filter: filterValue as RichTextV2Filter, + value: record[filterKey], + }); + } + case FieldMetadataType.SELECT: + return isMatchingSelectFilter({ + selectFilter: filterValue as SelectFilter, + value: record[filterKey], + }); + case FieldMetadataType.MULTI_SELECT: + return isMatchingMultiSelectFilter({ + multiSelectFilter: filterValue as MultiSelectFilter, + value: record[filterKey], + }); + case FieldMetadataType.ARRAY: { + return isMatchingArrayFilter({ + arrayFilter: filterValue as ArrayFilter, + value: record[filterKey], + }); + } + case FieldMetadataType.RAW_JSON: { + return isMatchingRawJsonFilter({ + rawJsonFilter: filterValue as RawJsonFilter, + value: record[filterKey], + }); + } + case FieldMetadataType.FULL_NAME: { + const fullNameFilter = filterValue as FullNameFilter; + + return ( + (fullNameFilter.firstName === undefined || + isMatchingStringFilter({ + stringFilter: fullNameFilter.firstName, + value: record[filterKey].firstName, + })) && + (fullNameFilter.lastName === undefined || + isMatchingStringFilter({ + stringFilter: fullNameFilter.lastName, + value: record[filterKey].lastName, + })) + ); + } + case FieldMetadataType.ADDRESS: { + const addressFilter = filterValue as AddressFilter; + + const keys = [ + 'addressStreet1', + 'addressStreet2', + 'addressCity', + 'addressState', + 'addressCountry', + 'addressPostcode', + ] as const; + + return keys.some((key) => { + const value = addressFilter[key]; + + if (value === undefined) { + return false; + } + + return isMatchingStringFilter({ + stringFilter: value, + value: record[filterKey][key], + }); + }); + } + case FieldMetadataType.LINKS: { + const linksFilter = filterValue as LinksFilter; + + const keys = ['primaryLinkLabel', 'primaryLinkUrl'] as const; + + return keys.some((key) => { + const value = linksFilter[key]; + + if (value === undefined) { + return false; + } + + return isMatchingStringFilter({ + stringFilter: value, + value: record[filterKey][key], + }); + }); + } + case FieldMetadataType.DATE: + case FieldMetadataType.DATE_TIME: { + return isMatchingDateFilter({ + dateFilter: filterValue as DateFilter, + value: record[filterKey], + }); + } + case FieldMetadataType.NUMBER: + case FieldMetadataType.NUMERIC: { + return isMatchingFloatFilter({ + floatFilter: filterValue as FloatFilter, + value: record[filterKey], + }); + } + case FieldMetadataType.UUID: { + return isMatchingUUIDFilter({ + uuidFilter: filterValue as UUIDFilter, + value: record[filterKey], + }); + } + case FieldMetadataType.BOOLEAN: { + return isMatchingBooleanFilter({ + booleanFilter: filterValue as BooleanFilter, + value: record[filterKey], + }); + } + case FieldMetadataType.CURRENCY: { + return isMatchingCurrencyFilter({ + currencyFilter: filterValue as CurrencyFilter, + value: record[filterKey], + }); + } + case FieldMetadataType.ACTOR: { + const actorFilter = filterValue as ActorFilter; + + return ( + actorFilter.name === undefined || + isMatchingStringFilter({ + stringFilter: actorFilter.name, + value: record[filterKey].name, + }) + ); + } + case FieldMetadataType.EMAILS: { + const emailsFilter = filterValue as EmailsFilter; + + if (emailsFilter.primaryEmail === undefined) { + return false; + } + + return isMatchingStringFilter({ + stringFilter: emailsFilter.primaryEmail, + value: record[filterKey].primaryEmail, + }); + } + case FieldMetadataType.PHONES: { + const phonesFilter = filterValue as PhonesFilter; + + const keys: (keyof PhonesFilter)[] = ['primaryPhoneNumber']; + + return keys.some((key) => { + const value = phonesFilter[key]; + + if (value === undefined) { + return false; + } + + return isMatchingStringFilter({ + stringFilter: value, + value: record[filterKey][key], + }); + }); + } + case FieldMetadataType.RELATION: { + const isJoinColumn = + ( + objectMetadataField.settings as + | { joinColumnName?: string } + | undefined + )?.joinColumnName === filterKey; + + if (isJoinColumn) { + return isMatchingUUIDFilter({ + uuidFilter: filterValue as UUIDFilter, + value: record[filterKey], + }); + } + + throw new Error( + `Not implemented yet, use UUID filter instead on the corresponding "${filterKey}Id" field`, + ); + } + case FieldMetadataType.TS_VECTOR: { + return isMatchingTSVectorFilter({ + tsVectorFilter: filterValue as TSVectorFilter, + value: record[filterKey], + }); + } + default: { + throw new Error( + `Not implemented yet for field type "${objectMetadataField.type}"`, + ); + } + } + }); +}; diff --git a/packages/twenty-server/src/engine/twenty-orm/utils/twenty-orm-graphql-api-exception-handler.util.ts b/packages/twenty-server/src/engine/twenty-orm/utils/twenty-orm-graphql-api-exception-handler.util.ts index 948883ff0a..3e1e312dae 100644 --- a/packages/twenty-server/src/engine/twenty-orm/utils/twenty-orm-graphql-api-exception-handler.util.ts +++ b/packages/twenty-server/src/engine/twenty-orm/utils/twenty-orm-graphql-api-exception-handler.util.ts @@ -37,6 +37,7 @@ export const twentyORMGraphqlApiExceptionHandler = ( case TwentyORMExceptionCode.CONNECT_RECORD_NOT_FOUND: case TwentyORMExceptionCode.CONNECT_NOT_ALLOWED: case TwentyORMExceptionCode.CONNECT_UNIQUE_CONSTRAINT_ERROR: + case TwentyORMExceptionCode.RLS_VALIDATION_FAILED: case TwentyORMExceptionCode.TOO_MANY_RECORDS_TO_UPDATE: throw new UserInputError(error.message, { userFriendlyMessage: error.userFriendlyMessage, diff --git a/packages/twenty-server/src/engine/twenty-orm/utils/validate-rls-predicates-for-records.util.ts b/packages/twenty-server/src/engine/twenty-orm/utils/validate-rls-predicates-for-records.util.ts new file mode 100644 index 0000000000..14dcc77864 --- /dev/null +++ b/packages/twenty-server/src/engine/twenty-orm/utils/validate-rls-predicates-for-records.util.ts @@ -0,0 +1,75 @@ +/* @license Enterprise */ + +import { type ObjectLiteral } from 'typeorm'; + +import { type WorkspaceInternalContext } from 'src/engine/twenty-orm/interfaces/workspace-internal-context.interface'; + +import { type AuthContext } from 'src/engine/core-modules/auth/types/auth-context.type'; +import { type FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type'; +import { + TwentyORMException, + TwentyORMExceptionCode, +} from 'src/engine/twenty-orm/exceptions/twenty-orm.exception'; +import { buildRowLevelPermissionRecordFilter } from 'src/engine/twenty-orm/utils/build-row-level-permission-record-filter.util'; +import { isRecordMatchingRLSRowLevelPermissionPredicate } from 'src/engine/twenty-orm/utils/is-record-matching-rls-row-level-permission-predicate.util'; + +type ValidateRLSPredicatesForRecordsArgs = { + records: T[]; + objectMetadata: FlatObjectMetadata; + internalContext: WorkspaceInternalContext; + authContext: AuthContext; + shouldBypassPermissionChecks: boolean; + errorMessage?: string; +}; + +export const validateRLSPredicatesForRecords = ({ + records, + objectMetadata, + internalContext, + authContext, + shouldBypassPermissionChecks, + errorMessage = 'Record does not satisfy row-level security constraints of your current role', +}: ValidateRLSPredicatesForRecordsArgs): void => { + if (shouldBypassPermissionChecks) { + return; + } + + const roleId = authContext.userWorkspaceId + ? internalContext.userWorkspaceRoleMap[authContext.userWorkspaceId] + : undefined; + + if (!roleId) { + return; + } + + const recordFilter = buildRowLevelPermissionRecordFilter({ + flatRowLevelPermissionPredicateMaps: + internalContext.flatRowLevelPermissionPredicateMaps, + flatRowLevelPermissionPredicateGroupMaps: + internalContext.flatRowLevelPermissionPredicateGroupMaps, + flatFieldMetadataMaps: internalContext.flatFieldMetadataMaps, + objectMetadata, + roleId, + authContext, + }); + + if (!recordFilter || Object.keys(recordFilter).length === 0) { + return; + } + + for (const record of records) { + const matchesRLS = isRecordMatchingRLSRowLevelPermissionPredicate({ + record, + filter: recordFilter, + flatObjectMetadata: objectMetadata, + flatFieldMetadataMaps: internalContext.flatFieldMetadataMaps, + }); + + if (!matchesRLS) { + throw new TwentyORMException( + errorMessage, + TwentyORMExceptionCode.RLS_VALIDATION_FAILED, + ); + } + } +}; diff --git a/packages/twenty-server/src/engine/workspace-manager/twenty-standard-application/utils/role-metadata/create-standard-role-flat-metadata.util.ts b/packages/twenty-server/src/engine/workspace-manager/twenty-standard-application/utils/role-metadata/create-standard-role-flat-metadata.util.ts index a8d5c0cb0e..35b971f308 100644 --- a/packages/twenty-server/src/engine/workspace-manager/twenty-standard-application/utils/role-metadata/create-standard-role-flat-metadata.util.ts +++ b/packages/twenty-server/src/engine/workspace-manager/twenty-standard-application/utils/role-metadata/create-standard-role-flat-metadata.util.ts @@ -74,5 +74,7 @@ export const createStandardRoleFlatMetadata = ({ fieldPermissionIds: [], objectPermissionIds: [], roleTargetIds: [], + rowLevelPermissionPredicateIds: [], + rowLevelPermissionPredicateGroupIds: [], }; }; diff --git a/packages/twenty-shared/src/types/ObjectPermissions.ts b/packages/twenty-shared/src/types/ObjectPermissions.ts index f0ef3ef7ab..771347aaa2 100644 --- a/packages/twenty-shared/src/types/ObjectPermissions.ts +++ b/packages/twenty-shared/src/types/ObjectPermissions.ts @@ -1,4 +1,6 @@ import { type RestrictedFieldsPermissions } from './RestrictedFieldsPermissions'; +import { type RowLevelPermissionPredicate } from './RowLevelPermissionPredicate'; +import { type RowLevelPermissionPredicateGroup } from './RowLevelPermissionPredicateGroup'; export type ObjectPermissions = { canReadObjectRecords: boolean; @@ -6,4 +8,6 @@ export type ObjectPermissions = { canSoftDeleteObjectRecords: boolean; canDestroyObjectRecords: boolean; restrictedFields: RestrictedFieldsPermissions; + rowLevelPermissionPredicates: RowLevelPermissionPredicate[]; + rowLevelPermissionPredicateGroups: RowLevelPermissionPredicateGroup[]; }; diff --git a/packages/twenty-shared/src/types/RowLevelPermissionPredicate.ts b/packages/twenty-shared/src/types/RowLevelPermissionPredicate.ts new file mode 100644 index 0000000000..736743378b --- /dev/null +++ b/packages/twenty-shared/src/types/RowLevelPermissionPredicate.ts @@ -0,0 +1,14 @@ +import { type RowLevelPermissionPredicateOperand } from '@/types/RowLevelPermissionPredicateOperand'; +import { type RowLevelPermissionPredicateValue } from '@/types/RowLevelPermissionPredicateValue'; + +export type RowLevelPermissionPredicate = { + id: string; + fieldMetadataId: string; + objectMetadataId: string; + operand: RowLevelPermissionPredicateOperand; + value: RowLevelPermissionPredicateValue; + subFieldName: string | null; + workspaceMemberFieldMetadataId: string | null; + workspaceMemberSubFieldName: string | null; + roleId: string; +}; diff --git a/packages/twenty-shared/src/types/RowLevelPermissionPredicateGroup.ts b/packages/twenty-shared/src/types/RowLevelPermissionPredicateGroup.ts new file mode 100644 index 0000000000..55ff1accad --- /dev/null +++ b/packages/twenty-shared/src/types/RowLevelPermissionPredicateGroup.ts @@ -0,0 +1,10 @@ +import { type RowLevelPermissionPredicateGroupLogicalOperator } from '@/types/RowLevelPermissionPredicateGroupLogicalOperator'; + +export type RowLevelPermissionPredicateGroup = { + id: string; + logicalOperator: RowLevelPermissionPredicateGroupLogicalOperator; + objectMetadataId: string; + parentRowLevelPermissionPredicateGroupId: string | null; + positionInRowLevelPermissionPredicateGroup: number | null; + roleId: string; +}; diff --git a/packages/twenty-server/src/engine/metadata-modules/row-level-permission-predicate/enums/row-level-permission-predicate-group-logical-operator.enum.ts b/packages/twenty-shared/src/types/RowLevelPermissionPredicateGroupLogicalOperator.ts similarity index 100% rename from packages/twenty-server/src/engine/metadata-modules/row-level-permission-predicate/enums/row-level-permission-predicate-group-logical-operator.enum.ts rename to packages/twenty-shared/src/types/RowLevelPermissionPredicateGroupLogicalOperator.ts diff --git a/packages/twenty-server/src/engine/metadata-modules/row-level-permission-predicate/enums/row-level-permission-predicate-operand.ts b/packages/twenty-shared/src/types/RowLevelPermissionPredicateOperand.ts similarity index 100% rename from packages/twenty-server/src/engine/metadata-modules/row-level-permission-predicate/enums/row-level-permission-predicate-operand.ts rename to packages/twenty-shared/src/types/RowLevelPermissionPredicateOperand.ts diff --git a/packages/twenty-server/src/engine/metadata-modules/row-level-permission-predicate/types/row-level-permission-predicate-value.type.ts b/packages/twenty-shared/src/types/RowLevelPermissionPredicateValue.ts similarity index 100% rename from packages/twenty-server/src/engine/metadata-modules/row-level-permission-predicate/types/row-level-permission-predicate-value.type.ts rename to packages/twenty-shared/src/types/RowLevelPermissionPredicateValue.ts diff --git a/packages/twenty-shared/src/types/index.ts b/packages/twenty-shared/src/types/index.ts index d7d3e59387..0411db5b1a 100644 --- a/packages/twenty-shared/src/types/index.ts +++ b/packages/twenty-shared/src/types/index.ts @@ -188,6 +188,14 @@ export { RelationType } from './RelationType'; export type { RelationUpdatePayload } from './RelationUpdatePayload'; export type { RestrictedFieldPermissions } from './RestrictedFieldPermissions'; export type { RestrictedFieldsPermissions } from './RestrictedFieldsPermissions'; +export type { RowLevelPermissionPredicate } from './RowLevelPermissionPredicate'; +export type { RowLevelPermissionPredicateGroup } from './RowLevelPermissionPredicateGroup'; +export { RowLevelPermissionPredicateGroupLogicalOperator } from './RowLevelPermissionPredicateGroupLogicalOperator'; +export { RowLevelPermissionPredicateOperand } from './RowLevelPermissionPredicateOperand'; +export type { + RelationPredicateValue, + RowLevelPermissionPredicateValue, +} from './RowLevelPermissionPredicateValue'; export { SettingsPath } from './SettingsPath'; export type { Sources } from './SourcesType'; export type { diff --git a/packages/twenty-shared/src/utils/filter/index.ts b/packages/twenty-shared/src/utils/filter/index.ts index 3e3c97fb28..9a2f8040db 100644 --- a/packages/twenty-shared/src/utils/filter/index.ts +++ b/packages/twenty-shared/src/utils/filter/index.ts @@ -15,6 +15,19 @@ export * from './utils/generateILikeFiltersForCompositeFields'; export * from './utils/getEmptyRecordGqlOperationFilter'; export * from './utils/getFilterTypeFromFieldType'; export * from './utils/isExpectedSubFieldName'; +export * from './utils/isMatchingArrayFilter'; +export * from './utils/isMatchingBooleanFilter'; +export * from './utils/isMatchingCurrencyFilter'; +export * from './utils/isMatchingDateFilter'; +export * from './utils/isMatchingFloatFilter'; +export * from './utils/isMatchingMultiSelectFilter'; +export * from './utils/isMatchingRatingFilter'; +export * from './utils/isMatchingRawJsonFilter'; +export * from './utils/isMatchingRichTextV2Filter'; +export * from './utils/isMatchingSelectFilter'; +export * from './utils/isMatchingStringFilter'; +export * from './utils/isMatchingTSVectorFilter'; +export * from './utils/isMatchingUUIDFilter'; export * from './utils/validation-schemas/arrayOfStringsOrVariablesSchema'; export * from './utils/validation-schemas/arrayOfUuidsOrVariablesSchema'; export * from './utils/validation-schemas/jsonRelationFilterValueSchema'; diff --git a/packages/twenty-front/src/modules/object-record/record-filter/utils/__tests__/isMatchingArrayFilter.test.ts b/packages/twenty-shared/src/utils/filter/utils/__tests__/isMatchingArrayFilter.test.ts similarity index 97% rename from packages/twenty-front/src/modules/object-record/record-filter/utils/__tests__/isMatchingArrayFilter.test.ts rename to packages/twenty-shared/src/utils/filter/utils/__tests__/isMatchingArrayFilter.test.ts index 12d4ad3704..a3867257f3 100644 --- a/packages/twenty-front/src/modules/object-record/record-filter/utils/__tests__/isMatchingArrayFilter.test.ts +++ b/packages/twenty-shared/src/utils/filter/utils/__tests__/isMatchingArrayFilter.test.ts @@ -1,4 +1,4 @@ -import { isMatchingArrayFilter } from '@/object-record/record-filter/utils/isMatchingArrayFilter'; +import { isMatchingArrayFilter } from '@/utils/filter/utils/isMatchingArrayFilter'; describe('isMatchingArrayFilter', () => { describe('is filter', () => { diff --git a/packages/twenty-front/src/modules/object-record/record-filter/utils/__tests__/isMatchingBooleanFilter.test.ts b/packages/twenty-shared/src/utils/filter/utils/__tests__/isMatchingBooleanFilter.test.ts similarity index 90% rename from packages/twenty-front/src/modules/object-record/record-filter/utils/__tests__/isMatchingBooleanFilter.test.ts rename to packages/twenty-shared/src/utils/filter/utils/__tests__/isMatchingBooleanFilter.test.ts index c6ad871587..80b0e21bd3 100644 --- a/packages/twenty-front/src/modules/object-record/record-filter/utils/__tests__/isMatchingBooleanFilter.test.ts +++ b/packages/twenty-shared/src/utils/filter/utils/__tests__/isMatchingBooleanFilter.test.ts @@ -1,4 +1,4 @@ -import { isMatchingBooleanFilter } from '@/object-record/record-filter/utils/isMatchingBooleanFilter'; +import { isMatchingBooleanFilter } from '@/utils/filter/utils/isMatchingBooleanFilter'; describe('isMatchingBooleanFilter', () => { describe('eq', () => { diff --git a/packages/twenty-front/src/modules/object-record/record-filter/utils/__tests__/isMatchingCurrencyFilter.test.ts b/packages/twenty-shared/src/utils/filter/utils/__tests__/isMatchingCurrencyFilter.test.ts similarity index 98% rename from packages/twenty-front/src/modules/object-record/record-filter/utils/__tests__/isMatchingCurrencyFilter.test.ts rename to packages/twenty-shared/src/utils/filter/utils/__tests__/isMatchingCurrencyFilter.test.ts index 84533ddda6..06cd1472ff 100644 --- a/packages/twenty-front/src/modules/object-record/record-filter/utils/__tests__/isMatchingCurrencyFilter.test.ts +++ b/packages/twenty-shared/src/utils/filter/utils/__tests__/isMatchingCurrencyFilter.test.ts @@ -1,5 +1,5 @@ -import { isMatchingCurrencyFilter } from '@/object-record/record-filter/utils/isMatchingCurrencyFilter'; -import { type CurrencyFilter } from 'twenty-shared/types'; +import { type CurrencyFilter } from '@/types'; +import { isMatchingCurrencyFilter } from '@/utils/filter/utils/isMatchingCurrencyFilter'; describe('isMatchingCurrencyFilter', () => { describe('amountMicros', () => { diff --git a/packages/twenty-front/src/modules/object-record/record-filter/utils/__tests__/isMatchingDateFilter.test.ts b/packages/twenty-shared/src/utils/filter/utils/__tests__/isMatchingDateFilter.test.ts similarity index 97% rename from packages/twenty-front/src/modules/object-record/record-filter/utils/__tests__/isMatchingDateFilter.test.ts rename to packages/twenty-shared/src/utils/filter/utils/__tests__/isMatchingDateFilter.test.ts index 9cd4b07d1e..d359bc34a6 100644 --- a/packages/twenty-front/src/modules/object-record/record-filter/utils/__tests__/isMatchingDateFilter.test.ts +++ b/packages/twenty-shared/src/utils/filter/utils/__tests__/isMatchingDateFilter.test.ts @@ -1,4 +1,4 @@ -import { isMatchingDateFilter } from '@/object-record/record-filter/utils/isMatchingDateFilter'; +import { isMatchingDateFilter } from '@/utils/filter/utils/isMatchingDateFilter'; describe('isMatchingDateFilter', () => { const testDate = '2023-12-19T12:15:29.810Z'; diff --git a/packages/twenty-front/src/modules/object-record/record-filter/utils/__tests__/isMatchingFloatFilter.test.ts b/packages/twenty-shared/src/utils/filter/utils/__tests__/isMatchingFloatFilter.test.ts similarity index 96% rename from packages/twenty-front/src/modules/object-record/record-filter/utils/__tests__/isMatchingFloatFilter.test.ts rename to packages/twenty-shared/src/utils/filter/utils/__tests__/isMatchingFloatFilter.test.ts index 98a6e7c5dd..0abf8399b4 100644 --- a/packages/twenty-front/src/modules/object-record/record-filter/utils/__tests__/isMatchingFloatFilter.test.ts +++ b/packages/twenty-shared/src/utils/filter/utils/__tests__/isMatchingFloatFilter.test.ts @@ -1,4 +1,4 @@ -import { isMatchingFloatFilter } from '@/object-record/record-filter/utils/isMatchingFloatFilter'; +import { isMatchingFloatFilter } from '@/utils/filter/utils/isMatchingFloatFilter'; describe('isMatchingFloatFilter', () => { describe('eq', () => { diff --git a/packages/twenty-front/src/modules/object-record/record-filter/utils/__tests__/isMatchingStringFilter.test.ts b/packages/twenty-shared/src/utils/filter/utils/__tests__/isMatchingStringFilter.test.ts similarity index 97% rename from packages/twenty-front/src/modules/object-record/record-filter/utils/__tests__/isMatchingStringFilter.test.ts rename to packages/twenty-shared/src/utils/filter/utils/__tests__/isMatchingStringFilter.test.ts index ef396ebfc3..8590d6df7b 100644 --- a/packages/twenty-front/src/modules/object-record/record-filter/utils/__tests__/isMatchingStringFilter.test.ts +++ b/packages/twenty-shared/src/utils/filter/utils/__tests__/isMatchingStringFilter.test.ts @@ -1,4 +1,4 @@ -import { isMatchingStringFilter } from '@/object-record/record-filter/utils/isMatchingStringFilter'; +import { isMatchingStringFilter } from '@/utils/filter/utils/isMatchingStringFilter'; describe('isMatchingStringFilter', () => { describe('eq', () => { diff --git a/packages/twenty-front/src/modules/object-record/record-filter/utils/__tests__/isMatchingTSVectorFilter.test.ts b/packages/twenty-shared/src/utils/filter/utils/__tests__/isMatchingTSVectorFilter.test.ts similarity index 93% rename from packages/twenty-front/src/modules/object-record/record-filter/utils/__tests__/isMatchingTSVectorFilter.test.ts rename to packages/twenty-shared/src/utils/filter/utils/__tests__/isMatchingTSVectorFilter.test.ts index c015bfc4ee..69eb8bf66b 100644 --- a/packages/twenty-front/src/modules/object-record/record-filter/utils/__tests__/isMatchingTSVectorFilter.test.ts +++ b/packages/twenty-shared/src/utils/filter/utils/__tests__/isMatchingTSVectorFilter.test.ts @@ -1,4 +1,4 @@ -import { isMatchingTSVectorFilter } from '@/object-record/record-filter/utils/isMatchingTSVectorFilter'; +import { isMatchingTSVectorFilter } from '@/utils/filter/utils/isMatchingTSVectorFilter'; describe('isMatchingTSVectorFilter', () => { describe('search', () => { diff --git a/packages/twenty-front/src/modules/object-record/record-filter/utils/__tests__/isMatchingUUIDFilter.test.ts b/packages/twenty-shared/src/utils/filter/utils/__tests__/isMatchingUUIDFilter.test.ts similarity index 94% rename from packages/twenty-front/src/modules/object-record/record-filter/utils/__tests__/isMatchingUUIDFilter.test.ts rename to packages/twenty-shared/src/utils/filter/utils/__tests__/isMatchingUUIDFilter.test.ts index bbe18c1fb1..4ff4682a64 100644 --- a/packages/twenty-front/src/modules/object-record/record-filter/utils/__tests__/isMatchingUUIDFilter.test.ts +++ b/packages/twenty-shared/src/utils/filter/utils/__tests__/isMatchingUUIDFilter.test.ts @@ -1,4 +1,4 @@ -import { isMatchingUUIDFilter } from '@/object-record/record-filter/utils/isMatchingUUIDFilter'; +import { isMatchingUUIDFilter } from '@/utils/filter/utils/isMatchingUUIDFilter'; describe('isMatchingUUIDFilter', () => { const testUUID = '123e4567-e89b-12d3-a456-426655440000'; diff --git a/packages/twenty-front/src/modules/object-record/record-filter/utils/isMatchingArrayFilter.ts b/packages/twenty-shared/src/utils/filter/utils/isMatchingArrayFilter.ts similarity index 93% rename from packages/twenty-front/src/modules/object-record/record-filter/utils/isMatchingArrayFilter.ts rename to packages/twenty-shared/src/utils/filter/utils/isMatchingArrayFilter.ts index 05126e7371..c7380ebac4 100644 --- a/packages/twenty-front/src/modules/object-record/record-filter/utils/isMatchingArrayFilter.ts +++ b/packages/twenty-shared/src/utils/filter/utils/isMatchingArrayFilter.ts @@ -1,4 +1,4 @@ -import { type ArrayFilter } from 'twenty-shared/types'; +import { type ArrayFilter } from '@/types'; export const isMatchingArrayFilter = ({ arrayFilter, diff --git a/packages/twenty-front/src/modules/object-record/record-filter/utils/isMatchingBooleanFilter.ts b/packages/twenty-shared/src/utils/filter/utils/isMatchingBooleanFilter.ts similarity index 89% rename from packages/twenty-front/src/modules/object-record/record-filter/utils/isMatchingBooleanFilter.ts rename to packages/twenty-shared/src/utils/filter/utils/isMatchingBooleanFilter.ts index 98cf6b9eb8..26f97a530e 100644 --- a/packages/twenty-front/src/modules/object-record/record-filter/utils/isMatchingBooleanFilter.ts +++ b/packages/twenty-shared/src/utils/filter/utils/isMatchingBooleanFilter.ts @@ -1,4 +1,4 @@ -import { type BooleanFilter } from 'twenty-shared/types'; +import { type BooleanFilter } from '@/types'; export const isMatchingBooleanFilter = ({ booleanFilter, diff --git a/packages/twenty-front/src/modules/object-record/record-filter/utils/isMatchingCurrencyFilter.ts b/packages/twenty-shared/src/utils/filter/utils/isMatchingCurrencyFilter.ts similarity index 96% rename from packages/twenty-front/src/modules/object-record/record-filter/utils/isMatchingCurrencyFilter.ts rename to packages/twenty-shared/src/utils/filter/utils/isMatchingCurrencyFilter.ts index 2ac8b32b1d..f3f6bab13d 100644 --- a/packages/twenty-front/src/modules/object-record/record-filter/utils/isMatchingCurrencyFilter.ts +++ b/packages/twenty-shared/src/utils/filter/utils/isMatchingCurrencyFilter.ts @@ -1,6 +1,6 @@ +import { type CurrencyFilter } from '@/types'; +import { isDefined } from '@/utils'; import { isNonEmptyString } from '@sniptt/guards'; -import { type CurrencyFilter } from 'twenty-shared/types'; -import { isDefined } from 'twenty-shared/utils'; const isMatchingCurrencyCodeFilter = ( currencyCodeFilter: CurrencyFilter['currencyCode'], diff --git a/packages/twenty-front/src/modules/object-record/record-filter/utils/isMatchingDateFilter.ts b/packages/twenty-shared/src/utils/filter/utils/isMatchingDateFilter.ts similarity index 96% rename from packages/twenty-front/src/modules/object-record/record-filter/utils/isMatchingDateFilter.ts rename to packages/twenty-shared/src/utils/filter/utils/isMatchingDateFilter.ts index 0693a66fbe..78492810a8 100644 --- a/packages/twenty-front/src/modules/object-record/record-filter/utils/isMatchingDateFilter.ts +++ b/packages/twenty-shared/src/utils/filter/utils/isMatchingDateFilter.ts @@ -1,5 +1,5 @@ +import { type DateFilter } from '@/types'; import { isAfter, isBefore, isEqual, parseISO } from 'date-fns'; -import { type DateFilter } from 'twenty-shared/types'; export const isMatchingDateFilter = ({ dateFilter, diff --git a/packages/twenty-front/src/modules/object-record/record-filter/utils/isMatchingFloatFilter.ts b/packages/twenty-shared/src/utils/filter/utils/isMatchingFloatFilter.ts similarity index 94% rename from packages/twenty-front/src/modules/object-record/record-filter/utils/isMatchingFloatFilter.ts rename to packages/twenty-shared/src/utils/filter/utils/isMatchingFloatFilter.ts index c3cca636f6..4d69c1a1a8 100644 --- a/packages/twenty-front/src/modules/object-record/record-filter/utils/isMatchingFloatFilter.ts +++ b/packages/twenty-shared/src/utils/filter/utils/isMatchingFloatFilter.ts @@ -1,4 +1,4 @@ -import { type FloatFilter } from 'twenty-shared/types'; +import { type FloatFilter } from '@/types'; export const isMatchingFloatFilter = ({ floatFilter, diff --git a/packages/twenty-front/src/modules/object-record/record-filter/utils/isMatchingMultiSelectFilter.ts b/packages/twenty-shared/src/utils/filter/utils/isMatchingMultiSelectFilter.ts similarity index 93% rename from packages/twenty-front/src/modules/object-record/record-filter/utils/isMatchingMultiSelectFilter.ts rename to packages/twenty-shared/src/utils/filter/utils/isMatchingMultiSelectFilter.ts index ce8b3760ad..4595f7b36e 100644 --- a/packages/twenty-front/src/modules/object-record/record-filter/utils/isMatchingMultiSelectFilter.ts +++ b/packages/twenty-shared/src/utils/filter/utils/isMatchingMultiSelectFilter.ts @@ -1,4 +1,4 @@ -import { type MultiSelectFilter } from 'twenty-shared/types'; +import { type MultiSelectFilter } from '@/types'; export const isMatchingMultiSelectFilter = ({ multiSelectFilter, diff --git a/packages/twenty-front/src/modules/object-record/record-filter/utils/isMatchingRatingFilter.ts b/packages/twenty-shared/src/utils/filter/utils/isMatchingRatingFilter.ts similarity index 92% rename from packages/twenty-front/src/modules/object-record/record-filter/utils/isMatchingRatingFilter.ts rename to packages/twenty-shared/src/utils/filter/utils/isMatchingRatingFilter.ts index 9bf64140b2..faf12164ca 100644 --- a/packages/twenty-front/src/modules/object-record/record-filter/utils/isMatchingRatingFilter.ts +++ b/packages/twenty-shared/src/utils/filter/utils/isMatchingRatingFilter.ts @@ -1,4 +1,4 @@ -import { type RatingFilter } from 'twenty-shared/types'; +import { type RatingFilter } from '@/types'; export const isMatchingRatingFilter = ({ ratingFilter, diff --git a/packages/twenty-front/src/modules/object-record/record-filter/utils/isMatchingRawJsonFilter.ts b/packages/twenty-shared/src/utils/filter/utils/isMatchingRawJsonFilter.ts similarity index 92% rename from packages/twenty-front/src/modules/object-record/record-filter/utils/isMatchingRawJsonFilter.ts rename to packages/twenty-shared/src/utils/filter/utils/isMatchingRawJsonFilter.ts index ea04317bae..de294aa1b7 100644 --- a/packages/twenty-front/src/modules/object-record/record-filter/utils/isMatchingRawJsonFilter.ts +++ b/packages/twenty-shared/src/utils/filter/utils/isMatchingRawJsonFilter.ts @@ -1,4 +1,4 @@ -import { type RawJsonFilter } from 'twenty-shared/types'; +import { type RawJsonFilter } from '@/types'; export const isMatchingRawJsonFilter = ({ rawJsonFilter, diff --git a/packages/twenty-front/src/modules/object-record/record-filter/utils/isMatchingRichTextV2Filter.ts b/packages/twenty-shared/src/utils/filter/utils/isMatchingRichTextV2Filter.ts similarity index 91% rename from packages/twenty-front/src/modules/object-record/record-filter/utils/isMatchingRichTextV2Filter.ts rename to packages/twenty-shared/src/utils/filter/utils/isMatchingRichTextV2Filter.ts index 89f3bb9c12..0fe7b9a1a3 100644 --- a/packages/twenty-front/src/modules/object-record/record-filter/utils/isMatchingRichTextV2Filter.ts +++ b/packages/twenty-shared/src/utils/filter/utils/isMatchingRichTextV2Filter.ts @@ -1,5 +1,5 @@ +import { type RichTextV2Filter } from '@/types'; import escapeRegExp from 'lodash.escaperegexp'; -import { type RichTextV2Filter } from 'twenty-shared/types'; export const isMatchingRichTextV2Filter = ({ richTextV2Filter, diff --git a/packages/twenty-front/src/modules/object-record/record-filter/utils/isMatchingSelectFilter.ts b/packages/twenty-shared/src/utils/filter/utils/isMatchingSelectFilter.ts similarity index 92% rename from packages/twenty-front/src/modules/object-record/record-filter/utils/isMatchingSelectFilter.ts rename to packages/twenty-shared/src/utils/filter/utils/isMatchingSelectFilter.ts index 23b2cdf465..468c75162d 100644 --- a/packages/twenty-front/src/modules/object-record/record-filter/utils/isMatchingSelectFilter.ts +++ b/packages/twenty-shared/src/utils/filter/utils/isMatchingSelectFilter.ts @@ -1,4 +1,4 @@ -import { type SelectFilter } from 'twenty-shared/types'; +import { type SelectFilter } from '@/types'; export const isMatchingSelectFilter = ({ selectFilter, diff --git a/packages/twenty-front/src/modules/object-record/record-filter/utils/isMatchingStringFilter.ts b/packages/twenty-shared/src/utils/filter/utils/isMatchingStringFilter.ts similarity index 97% rename from packages/twenty-front/src/modules/object-record/record-filter/utils/isMatchingStringFilter.ts rename to packages/twenty-shared/src/utils/filter/utils/isMatchingStringFilter.ts index 431732765e..7de9d0af12 100644 --- a/packages/twenty-front/src/modules/object-record/record-filter/utils/isMatchingStringFilter.ts +++ b/packages/twenty-shared/src/utils/filter/utils/isMatchingStringFilter.ts @@ -1,5 +1,5 @@ +import { type StringFilter } from '@/types'; import escapeRegExp from 'lodash.escaperegexp'; -import { type StringFilter } from 'twenty-shared/types'; export const isMatchingStringFilter = ({ stringFilter, diff --git a/packages/twenty-front/src/modules/object-record/record-filter/utils/isMatchingTSVectorFilter.ts b/packages/twenty-shared/src/utils/filter/utils/isMatchingTSVectorFilter.ts similarity index 92% rename from packages/twenty-front/src/modules/object-record/record-filter/utils/isMatchingTSVectorFilter.ts rename to packages/twenty-shared/src/utils/filter/utils/isMatchingTSVectorFilter.ts index 89c678ccd1..2397c68456 100644 --- a/packages/twenty-front/src/modules/object-record/record-filter/utils/isMatchingTSVectorFilter.ts +++ b/packages/twenty-shared/src/utils/filter/utils/isMatchingTSVectorFilter.ts @@ -1,4 +1,4 @@ -import { type TSVectorFilter } from 'twenty-shared/types'; +import { type TSVectorFilter } from '@/types'; export const isMatchingTSVectorFilter = ({ tsVectorFilter, diff --git a/packages/twenty-front/src/modules/object-record/record-filter/utils/isMatchingUUIDFilter.ts b/packages/twenty-shared/src/utils/filter/utils/isMatchingUUIDFilter.ts similarity index 90% rename from packages/twenty-front/src/modules/object-record/record-filter/utils/isMatchingUUIDFilter.ts rename to packages/twenty-shared/src/utils/filter/utils/isMatchingUUIDFilter.ts index af34e0bef8..754b7a99c5 100644 --- a/packages/twenty-front/src/modules/object-record/record-filter/utils/isMatchingUUIDFilter.ts +++ b/packages/twenty-shared/src/utils/filter/utils/isMatchingUUIDFilter.ts @@ -1,4 +1,4 @@ -import { type UUIDFilter, type UUIDFilterValue } from 'twenty-shared/types'; +import { type UUIDFilter, type UUIDFilterValue } from '@/types'; export const isMatchingUUIDFilter = ({ uuidFilter, diff --git a/packages/twenty-shared/src/utils/index.ts b/packages/twenty-shared/src/utils/index.ts index dd343f9036..0a399481e8 100644 --- a/packages/twenty-shared/src/utils/index.ts +++ b/packages/twenty-shared/src/utils/index.ts @@ -103,6 +103,19 @@ export { generateILikeFiltersForCompositeFields } from './filter/utils/generateI export { getEmptyRecordGqlOperationFilter } from './filter/utils/getEmptyRecordGqlOperationFilter'; export { getFilterTypeFromFieldType } from './filter/utils/getFilterTypeFromFieldType'; export { isExpectedSubFieldName } from './filter/utils/isExpectedSubFieldName'; +export { isMatchingArrayFilter } from './filter/utils/isMatchingArrayFilter'; +export { isMatchingBooleanFilter } from './filter/utils/isMatchingBooleanFilter'; +export { isMatchingCurrencyFilter } from './filter/utils/isMatchingCurrencyFilter'; +export { isMatchingDateFilter } from './filter/utils/isMatchingDateFilter'; +export { isMatchingFloatFilter } from './filter/utils/isMatchingFloatFilter'; +export { isMatchingMultiSelectFilter } from './filter/utils/isMatchingMultiSelectFilter'; +export { isMatchingRatingFilter } from './filter/utils/isMatchingRatingFilter'; +export { isMatchingRawJsonFilter } from './filter/utils/isMatchingRawJsonFilter'; +export { isMatchingRichTextV2Filter } from './filter/utils/isMatchingRichTextV2Filter'; +export { isMatchingSelectFilter } from './filter/utils/isMatchingSelectFilter'; +export { isMatchingStringFilter } from './filter/utils/isMatchingStringFilter'; +export { isMatchingTSVectorFilter } from './filter/utils/isMatchingTSVectorFilter'; +export { isMatchingUUIDFilter } from './filter/utils/isMatchingUUIDFilter'; export { arrayOfStringsOrVariablesSchema } from './filter/utils/validation-schemas/arrayOfStringsOrVariablesSchema'; export { arrayOfUuidOrVariableSchema } from './filter/utils/validation-schemas/arrayOfUuidsOrVariablesSchema'; export {