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 {