diff --git a/packages/twenty-front/src/generated-metadata/graphql.ts b/packages/twenty-front/src/generated-metadata/graphql.ts index 6f9815e695..5122636bb2 100644 --- a/packages/twenty-front/src/generated-metadata/graphql.ts +++ b/packages/twenty-front/src/generated-metadata/graphql.ts @@ -436,6 +436,18 @@ export type BillingEndTrialPeriodOutput = { status?: Maybe; }; +export type BillingEntitlement = { + __typename?: 'BillingEntitlement'; + key: BillingEntitlementKey; + value: Scalars['Boolean']; +}; + +export enum BillingEntitlementKey { + CUSTOM_DOMAIN = 'CUSTOM_DOMAIN', + RLS = 'RLS', + SSO = 'SSO' +} + export type BillingLicensedProduct = BillingProductDto & { __typename?: 'BillingLicensedProduct'; description: Scalars['String']; @@ -5365,6 +5377,7 @@ export type Workspace = { __typename?: 'Workspace'; activationStatus: WorkspaceActivationStatus; allowImpersonation: Scalars['Boolean']; + billingEntitlements: Array; billingSubscriptions: Array; createdAt: Scalars['DateTime']; currentBillingSubscription?: Maybe; @@ -6548,7 +6561,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, 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 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 }> }> }>, billingEntitlements: Array<{ __typename?: 'BillingEntitlement', key: BillingEntitlementKey, value: boolean }>, 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 }; @@ -6567,7 +6580,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, 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 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 }> }> }>, billingEntitlements: Array<{ __typename?: 'BillingEntitlement', key: BillingEntitlementKey, value: boolean }>, 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 }; @@ -7602,6 +7615,10 @@ export const UserQueryFragmentFragmentDoc = gql` billingSubscriptions { ...BillingSubscriptionFragment } + billingEntitlements { + key + value + } workspaceMembersCount defaultRole { ...RoleFragment diff --git a/packages/twenty-front/src/generated/graphql.ts b/packages/twenty-front/src/generated/graphql.ts index 60e136d832..4fb4dd8f3f 100644 --- a/packages/twenty-front/src/generated/graphql.ts +++ b/packages/twenty-front/src/generated/graphql.ts @@ -436,6 +436,18 @@ export type BillingEndTrialPeriodOutput = { status?: Maybe; }; +export type BillingEntitlement = { + __typename?: 'BillingEntitlement'; + key: BillingEntitlementKey; + value: Scalars['Boolean']; +}; + +export enum BillingEntitlementKey { + CUSTOM_DOMAIN = 'CUSTOM_DOMAIN', + RLS = 'RLS', + SSO = 'SSO' +} + export type BillingLicensedProduct = BillingProductDto & { __typename?: 'BillingLicensedProduct'; description: Scalars['String']; @@ -5188,6 +5200,7 @@ export type Workspace = { __typename?: 'Workspace'; activationStatus: WorkspaceActivationStatus; allowImpersonation: Scalars['Boolean']; + billingEntitlements: Array; billingSubscriptions: Array; createdAt: Scalars['DateTime']; currentBillingSubscription?: Maybe; diff --git a/packages/twenty-front/src/modules/apollo/services/__tests__/apollo.factory.test.ts b/packages/twenty-front/src/modules/apollo/services/__tests__/apollo.factory.test.ts index b29eca7071..33f49dc695 100644 --- a/packages/twenty-front/src/modules/apollo/services/__tests__/apollo.factory.test.ts +++ b/packages/twenty-front/src/modules/apollo/services/__tests__/apollo.factory.test.ts @@ -3,9 +3,9 @@ import fetchMock, { enableFetchMocks } from 'jest-fetch-mock'; import { DEFAULT_FAST_MODEL } from '@/ai/constants/DefaultFastModel'; import { DEFAULT_SMART_MODEL } from '@/ai/constants/DefaultSmartModel'; +import { ApolloFactory, type Options } from '@/apollo/services/apollo.factory'; import { CUSTOM_WORKSPACE_APPLICATION_MOCK } from '@/object-metadata/hooks/__tests__/constants/CustomWorkspaceApplicationMock.test.constant'; import { WorkspaceActivationStatus } from '~/generated/graphql'; -import { ApolloFactory, type Options } from '@/apollo/services/apollo.factory'; enableFetchMocks(); @@ -45,6 +45,7 @@ const mockWorkspace = { allowImpersonation: false, activationStatus: WorkspaceActivationStatus.ACTIVE, billingSubscriptions: [], + billingEntitlements: [], currentBillingSubscription: null, workspaceMembersCount: 0, isPublicInviteLinkEnabled: false, diff --git a/packages/twenty-front/src/modules/auth/states/currentWorkspaceState.ts b/packages/twenty-front/src/modules/auth/states/currentWorkspaceState.ts index 38c584c982..b3d5dd0879 100644 --- a/packages/twenty-front/src/modules/auth/states/currentWorkspaceState.ts +++ b/packages/twenty-front/src/modules/auth/states/currentWorkspaceState.ts @@ -15,6 +15,7 @@ export type CurrentWorkspace = Pick< | 'featureFlags' | 'activationStatus' | 'billingSubscriptions' + | 'billingEntitlements' | 'currentBillingSubscription' | 'workspaceMembersCount' | 'isPublicInviteLinkEnabled' diff --git a/packages/twenty-front/src/modules/object-metadata/hooks/__tests__/useColumnDefinitionsFromObjectMetadata.test.ts b/packages/twenty-front/src/modules/object-metadata/hooks/__tests__/useColumnDefinitionsFromObjectMetadata.test.ts index 3aed48ae4b..01eba54882 100644 --- a/packages/twenty-front/src/modules/object-metadata/hooks/__tests__/useColumnDefinitionsFromObjectMetadata.test.ts +++ b/packages/twenty-front/src/modules/object-metadata/hooks/__tests__/useColumnDefinitionsFromObjectMetadata.test.ts @@ -1,11 +1,11 @@ import { renderHook } from '@testing-library/react'; +import { DEFAULT_FAST_MODEL } from '@/ai/constants/DefaultFastModel'; +import { DEFAULT_SMART_MODEL } from '@/ai/constants/DefaultSmartModel'; import { currentWorkspaceState } from '@/auth/states/currentWorkspaceState'; import { CUSTOM_WORKSPACE_APPLICATION_MOCK } from '@/object-metadata/hooks/__tests__/constants/CustomWorkspaceApplicationMock.test.constant'; import { useColumnDefinitionsFromObjectMetadata } from '@/object-metadata/hooks/useColumnDefinitionsFromObjectMetadata'; import { type ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem'; -import { DEFAULT_FAST_MODEL } from '@/ai/constants/DefaultFastModel'; -import { DEFAULT_SMART_MODEL } from '@/ai/constants/DefaultSmartModel'; import { SubscriptionInterval, SubscriptionStatus, @@ -50,6 +50,7 @@ const Wrapper = getJestMetadataAndApolloMocksAndActionMenuWrapper({ metadata: {}, phases: [], }, + billingEntitlements: [], billingSubscriptions: [ { id: '1', diff --git a/packages/twenty-front/src/modules/object-record/record-field/ui/form-types/components/FormSingleRecordFieldChip.tsx b/packages/twenty-front/src/modules/object-record/record-field/ui/form-types/components/FormSingleRecordFieldChip.tsx index 30f4f7f4f7..6eb92ae472 100644 --- a/packages/twenty-front/src/modules/object-record/record-field/ui/form-types/components/FormSingleRecordFieldChip.tsx +++ b/packages/twenty-front/src/modules/object-record/record-field/ui/form-types/components/FormSingleRecordFieldChip.tsx @@ -1,4 +1,3 @@ -import { t } from '@lingui/core/macro'; import { RecordChip } from '@/object-record/components/RecordChip'; import { FormFieldPlaceholder } from '@/object-record/record-field/ui/form-types/components/FormFieldPlaceholder'; import { @@ -9,6 +8,7 @@ import { VariableChipStandalone } from '@/object-record/record-field/ui/form-typ import { type ObjectRecord } from '@/object-record/types/ObjectRecord'; import { isStandaloneVariableString } from '@/workflow/utils/isStandaloneVariableString'; import styled from '@emotion/styled'; +import { t } from '@lingui/core/macro'; const StyledRecordChip = styled(RecordChip)` margin: ${({ theme }) => theme.spacing(2)}; @@ -64,7 +64,5 @@ export const FormSingleRecordFieldChip = ({ ); } - return ( - {t`Select a ${objectNameSingular}`} - ); + return {t`Select`}; }; diff --git a/packages/twenty-front/src/modules/object-record/record-field/ui/form-types/components/FormSingleRecordPicker.tsx b/packages/twenty-front/src/modules/object-record/record-field/ui/form-types/components/FormSingleRecordPicker.tsx index d27e0a66ec..ce3585df51 100644 --- a/packages/twenty-front/src/modules/object-record/record-field/ui/form-types/components/FormSingleRecordPicker.tsx +++ b/packages/twenty-front/src/modules/object-record/record-field/ui/form-types/components/FormSingleRecordPicker.tsx @@ -160,8 +160,6 @@ export const FormSingleRecordPicker = ({ } }; - const objectNames = objectNameSingulars.join(' or '); - return ( {label ? {label} : null} @@ -214,7 +212,7 @@ export const FormSingleRecordPicker = ({ focusId={dropdownId} componentInstanceId={dropdownId} EmptyIcon={IconForbid} - emptyLabel={t`No ${objectNames}`} + emptyLabel={t`No records`} onCancel={() => closeDropdown(dropdownId)} onMorphItemSelected={handleMorphItemSelected} objectNameSingulars={objectNameSingulars} diff --git a/packages/twenty-front/src/modules/object-record/record-field/ui/meta-types/display/components/__stories__/perf/DateTimeFieldDisplay.perf.stories.tsx b/packages/twenty-front/src/modules/object-record/record-field/ui/meta-types/display/components/__stories__/perf/DateTimeFieldDisplay.perf.stories.tsx index 7539d1e399..82d115d729 100644 --- a/packages/twenty-front/src/modules/object-record/record-field/ui/meta-types/display/components/__stories__/perf/DateTimeFieldDisplay.perf.stories.tsx +++ b/packages/twenty-front/src/modules/object-record/record-field/ui/meta-types/display/components/__stories__/perf/DateTimeFieldDisplay.perf.stories.tsx @@ -50,7 +50,7 @@ export const Elipsis: Story = { export const Performance = getProfilingStory({ componentName: 'DateTimeFieldDisplay', - averageThresholdInMs: 0.15, + averageThresholdInMs: 0.2, numberOfRuns: 30, numberOfTestsPerRun: 30, }); diff --git a/packages/twenty-front/src/modules/settings/roles/role-permissions/object-level-permissions/object-form/components/SettingsRolePermissionsObjectLevelObjectForm.tsx b/packages/twenty-front/src/modules/settings/roles/role-permissions/object-level-permissions/object-form/components/SettingsRolePermissionsObjectLevelObjectForm.tsx index b88c5e28e7..0b9ff849a1 100644 --- a/packages/twenty-front/src/modules/settings/roles/role-permissions/object-level-permissions/object-form/components/SettingsRolePermissionsObjectLevelObjectForm.tsx +++ b/packages/twenty-front/src/modules/settings/roles/role-permissions/object-level-permissions/object-form/components/SettingsRolePermissionsObjectLevelObjectForm.tsx @@ -15,6 +15,8 @@ import { SettingsPath, type ViewFilterOperand } from 'twenty-shared/types'; import { getSettingsPath, isDefined } from 'twenty-shared/utils'; import { Button } from 'twenty-ui/input'; import { + type BillingEntitlement, + BillingEntitlementKey, FeatureFlagKey, useFindOneAgentQuery, } from '~/generated-metadata/graphql'; @@ -48,10 +50,21 @@ export const SettingsRolePermissionsObjectLevelObjectForm = ({ }); const featureFlagsMap = useFeatureFlagsMap(); + + const workspaceBillingEntitlements = currentWorkspace?.billingEntitlements; + + const isRLSBillingEntitlementEnabled = + workspaceBillingEntitlements?.some( + (entitlement: BillingEntitlement) => + entitlement.key === BillingEntitlementKey.RLS && + entitlement.value === true, + ) ?? false; + const isRowLevelPermissionPredicatesEnabled = featureFlagsMap[ FeatureFlagKey.IS_ROW_LEVEL_PERMISSION_PREDICATES_ENABLED - ] && hasValidEnterpriseKey; + ] && + (hasValidEnterpriseKey || isRLSBillingEntitlementEnabled); const objectMetadataItem = objectMetadata.objectMetadataItem; diff --git a/packages/twenty-front/src/modules/settings/roles/role-permissions/object-level-permissions/record-level-permissions/components/SettingsRolePermissionsObjectLevelRecordLevelPermissionFilterRow.tsx b/packages/twenty-front/src/modules/settings/roles/role-permissions/object-level-permissions/record-level-permissions/components/SettingsRolePermissionsObjectLevelRecordLevelPermissionFilterRow.tsx index 95278df39f..57ce86f148 100644 --- a/packages/twenty-front/src/modules/settings/roles/role-permissions/object-level-permissions/record-level-permissions/components/SettingsRolePermissionsObjectLevelRecordLevelPermissionFilterRow.tsx +++ b/packages/twenty-front/src/modules/settings/roles/role-permissions/object-level-permissions/record-level-permissions/components/SettingsRolePermissionsObjectLevelRecordLevelPermissionFilterRow.tsx @@ -20,7 +20,7 @@ const StyledFilterRow = styled.div` `; const StyledOperandSelectContainer = styled.div` - width: 46px; + width: 50px; `; type SettingsRolePermissionsObjectLevelRecordLevelPermissionFilterRowProps = { diff --git a/packages/twenty-front/src/modules/settings/roles/role-permissions/object-level-permissions/record-level-permissions/components/SettingsRolePermissionsObjectLevelRecordLevelPermissionLogicalOperatorCell.tsx b/packages/twenty-front/src/modules/settings/roles/role-permissions/object-level-permissions/record-level-permissions/components/SettingsRolePermissionsObjectLevelRecordLevelPermissionLogicalOperatorCell.tsx index 91193ebca2..9db453ae1d 100644 --- a/packages/twenty-front/src/modules/settings/roles/role-permissions/object-level-permissions/record-level-permissions/components/SettingsRolePermissionsObjectLevelRecordLevelPermissionLogicalOperatorCell.tsx +++ b/packages/twenty-front/src/modules/settings/roles/role-permissions/object-level-permissions/record-level-permissions/components/SettingsRolePermissionsObjectLevelRecordLevelPermissionLogicalOperatorCell.tsx @@ -23,7 +23,7 @@ const StyledText = styled.div` const StyledContainer = styled.div` align-items: start; display: flex; - min-width: ${({ theme }) => theme.spacing(20)}; + min-width: ${({ theme }) => theme.spacing(16)}; color: ${({ theme }) => theme.font.color.tertiary}; `; diff --git a/packages/twenty-front/src/modules/users/graphql/fragments/userQueryFragment.ts b/packages/twenty-front/src/modules/users/graphql/fragments/userQueryFragment.ts index a0ba771ef4..6360ab35e4 100644 --- a/packages/twenty-front/src/modules/users/graphql/fragments/userQueryFragment.ts +++ b/packages/twenty-front/src/modules/users/graphql/fragments/userQueryFragment.ts @@ -78,6 +78,10 @@ export const USER_QUERY_FRAGMENT = gql` billingSubscriptions { ...BillingSubscriptionFragment } + billingEntitlements { + key + value + } workspaceMembersCount defaultRole { ...RoleFragment diff --git a/packages/twenty-front/src/testing/mock-data/users.ts b/packages/twenty-front/src/testing/mock-data/users.ts index 82a8f101cb..f77db60831 100644 --- a/packages/twenty-front/src/testing/mock-data/users.ts +++ b/packages/twenty-front/src/testing/mock-data/users.ts @@ -126,6 +126,7 @@ export const mockCurrentWorkspace = { }, ], }, + billingEntitlements: [], billingSubscriptions: [ { __typename: 'BillingSubscription', diff --git a/packages/twenty-server/src/engine/core-modules/billing-webhook/billing-webhook.module.ts b/packages/twenty-server/src/engine/core-modules/billing-webhook/billing-webhook.module.ts index da1ac55bf7..8dfec800c3 100644 --- a/packages/twenty-server/src/engine/core-modules/billing-webhook/billing-webhook.module.ts +++ b/packages/twenty-server/src/engine/core-modules/billing-webhook/billing-webhook.module.ts @@ -3,6 +3,7 @@ import { TypeOrmModule } from '@nestjs/typeorm'; import { BillingWebhookController } from 'src/engine/core-modules/billing-webhook/billing-webhook.controller'; import { BillingWebhookAlertService } from 'src/engine/core-modules/billing-webhook/services/billing-webhook-alert.service'; +import { BillingWebhookCreditGrantService } from 'src/engine/core-modules/billing-webhook/services/billing-webhook-credit-grant.service'; import { BillingWebhookCustomerService } from 'src/engine/core-modules/billing-webhook/services/billing-webhook-customer.service'; import { BillingWebhookEntitlementService } from 'src/engine/core-modules/billing-webhook/services/billing-webhook-entitlement.service'; import { BillingWebhookInvoiceService } from 'src/engine/core-modules/billing-webhook/services/billing-webhook-invoice.service'; @@ -10,7 +11,6 @@ import { BillingWebhookPriceService } from 'src/engine/core-modules/billing-webh import { BillingWebhookProductService } from 'src/engine/core-modules/billing-webhook/services/billing-webhook-product.service'; import { BillingWebhookSubscriptionScheduleService } from 'src/engine/core-modules/billing-webhook/services/billing-webhook-subscription-schedule.service'; import { BillingWebhookSubscriptionService } from 'src/engine/core-modules/billing-webhook/services/billing-webhook-subscription.service'; -import { BillingWebhookCreditGrantService } from 'src/engine/core-modules/billing-webhook/services/billing-webhook-credit-grant.service'; import { BillingModule } from 'src/engine/core-modules/billing/billing.module'; import { BillingCustomerEntity } from 'src/engine/core-modules/billing/entities/billing-customer.entity'; import { BillingEntitlementEntity } from 'src/engine/core-modules/billing/entities/billing-entitlement.entity'; @@ -27,6 +27,7 @@ import { UserWorkspaceEntity } from 'src/engine/core-modules/user-workspace/user import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity'; import { WorkspaceModule } from 'src/engine/core-modules/workspace/workspace.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'; @Module({ imports: [ @@ -48,6 +49,7 @@ import { PermissionsModule } from 'src/engine/metadata-modules/permissions/permi UserWorkspaceEntity, FeatureFlagEntity, ]), + RowLevelPermissionModule, ], controllers: [BillingWebhookController], providers: [ diff --git a/packages/twenty-server/src/engine/core-modules/billing-webhook/services/billing-webhook-entitlement.service.ts b/packages/twenty-server/src/engine/core-modules/billing-webhook/services/billing-webhook-entitlement.service.ts index 19c756a782..16657a03ed 100644 --- a/packages/twenty-server/src/engine/core-modules/billing-webhook/services/billing-webhook-entitlement.service.ts +++ b/packages/twenty-server/src/engine/core-modules/billing-webhook/services/billing-webhook-entitlement.service.ts @@ -14,6 +14,8 @@ import { } from 'src/engine/core-modules/billing/billing.exception'; import { BillingCustomerEntity } from 'src/engine/core-modules/billing/entities/billing-customer.entity'; import { BillingEntitlementEntity } from 'src/engine/core-modules/billing/entities/billing-entitlement.entity'; +import { BillingEntitlementKey } from 'src/engine/core-modules/billing/enums/billing-entitlement-key.enum'; +import { RowLevelPermissionPredicateGroupService } from 'src/engine/metadata-modules/row-level-permission-predicate/services/row-level-permission-predicate-group.service'; @Injectable() export class BillingWebhookEntitlementService { @@ -22,6 +24,7 @@ export class BillingWebhookEntitlementService { private readonly billingCustomerRepository: Repository, @InjectRepository(BillingEntitlementEntity) private readonly billingEntitlementRepository: Repository, + private readonly rowLevelPermissionPredicateGroupService: RowLevelPermissionPredicateGroupService, ) {} async processStripeEvent( @@ -40,17 +43,30 @@ export class BillingWebhookEntitlementService { const workspaceId = billingCustomer.workspaceId; - await this.billingEntitlementRepository.upsert( + const billingEntitlements = transformStripeEntitlementUpdatedEventToDatabaseEntitlement( workspaceId, data, - ), - { - conflictPaths: ['workspaceId', 'key'], - skipUpdateIfNoValuesChanged: true, - }, + ); + + await this.billingEntitlementRepository.upsert(billingEntitlements, { + conflictPaths: ['workspaceId', 'key'], + skipUpdateIfNoValuesChanged: true, + }); + + const isRowLevelPermissionDisabled = billingEntitlements.some( + (entitlement) => + entitlement.workspaceId === workspaceId && + entitlement.key === BillingEntitlementKey.RLS && + entitlement.value === false, ); + if (isRowLevelPermissionDisabled) { + await this.rowLevelPermissionPredicateGroupService.deleteAllRowLevelPermissionPredicateGroups( + workspaceId, + ); + } + return { stripeEntitlementCustomerId: data.object.customer, }; diff --git a/packages/twenty-server/src/engine/core-modules/billing-webhook/utils/__tests__/transform-stripe-entitlement-updated-event-to-database-entitlement.util.spec.ts b/packages/twenty-server/src/engine/core-modules/billing-webhook/utils/__tests__/transform-stripe-entitlement-updated-event-to-database-entitlement.util.spec.ts index e3ba4eedaf..ea6c2fe5d6 100644 --- a/packages/twenty-server/src/engine/core-modules/billing-webhook/utils/__tests__/transform-stripe-entitlement-updated-event-to-database-entitlement.util.spec.ts +++ b/packages/twenty-server/src/engine/core-modules/billing-webhook/utils/__tests__/transform-stripe-entitlement-updated-event-to-database-entitlement.util.spec.ts @@ -47,6 +47,12 @@ describe('transformStripeEntitlementUpdatedEventToDatabaseEntitlement', () => { value: false, workspaceId: 'workspaceId', }, + { + key: BillingEntitlementKey.RLS, + stripeCustomerId: 'cus_123', + value: false, + workspaceId: 'workspaceId', + }, ]); }); @@ -86,7 +92,13 @@ describe('transformStripeEntitlementUpdatedEventToDatabaseEntitlement', () => { stripeCustomerId: 'cus_123', }, { - key: 'CUSTOM_DOMAIN', + key: BillingEntitlementKey.CUSTOM_DOMAIN, + stripeCustomerId: 'cus_123', + value: false, + workspaceId: 'workspaceId', + }, + { + key: BillingEntitlementKey.RLS, stripeCustomerId: 'cus_123', value: false, workspaceId: 'workspaceId', diff --git a/packages/twenty-server/src/engine/core-modules/billing/dtos/billing-entitlement.dto.ts b/packages/twenty-server/src/engine/core-modules/billing/dtos/billing-entitlement.dto.ts new file mode 100644 index 0000000000..782b2be7a2 --- /dev/null +++ b/packages/twenty-server/src/engine/core-modules/billing/dtos/billing-entitlement.dto.ts @@ -0,0 +1,18 @@ +/* @license Enterprise */ + +import { Field, ObjectType, registerEnumType } from '@nestjs/graphql'; + +import { BillingEntitlementKey } from 'src/engine/core-modules/billing/enums/billing-entitlement-key.enum'; + +registerEnumType(BillingEntitlementKey, { + name: 'BillingEntitlementKey', +}); + +@ObjectType('BillingEntitlement') +export class BillingEntitlementDTO { + @Field(() => BillingEntitlementKey) + key: BillingEntitlementKey; + + @Field(() => Boolean) + value: boolean; +} diff --git a/packages/twenty-server/src/engine/core-modules/billing/enums/billing-entitlement-key.enum.ts b/packages/twenty-server/src/engine/core-modules/billing/enums/billing-entitlement-key.enum.ts index c946fe5990..63fc67f6b7 100644 --- a/packages/twenty-server/src/engine/core-modules/billing/enums/billing-entitlement-key.enum.ts +++ b/packages/twenty-server/src/engine/core-modules/billing/enums/billing-entitlement-key.enum.ts @@ -3,4 +3,5 @@ export enum BillingEntitlementKey { SSO = 'SSO', CUSTOM_DOMAIN = 'CUSTOM_DOMAIN', + RLS = 'RLS', } diff --git a/packages/twenty-server/src/engine/core-modules/billing/services/billing-subscription.service.ts b/packages/twenty-server/src/engine/core-modules/billing/services/billing-subscription.service.ts index 00980e72c1..b95fc9f296 100644 --- a/packages/twenty-server/src/engine/core-modules/billing/services/billing-subscription.service.ts +++ b/packages/twenty-server/src/engine/core-modules/billing/services/billing-subscription.service.ts @@ -175,6 +175,22 @@ export class BillingSubscriptionService { }; } + async getWorkspaceEntitlements( + workspaceId: string, + ): Promise { + const isBillingEnabled = this.twentyConfigService.get('IS_BILLING_ENABLED'); + + if (!isBillingEnabled) { + return []; + } + + const entitlements = await this.billingEntitlementRepository.find({ + where: { workspaceId }, + }); + + return entitlements; + } + async getWorkspaceEntitlementByKey( workspaceId: string, key: BillingEntitlementKey, diff --git a/packages/twenty-server/src/engine/core-modules/workspace/workspace.resolver.ts b/packages/twenty-server/src/engine/core-modules/workspace/workspace.resolver.ts index c46a4c5c3d..538834c137 100644 --- a/packages/twenty-server/src/engine/core-modules/workspace/workspace.resolver.ts +++ b/packages/twenty-server/src/engine/core-modules/workspace/workspace.resolver.ts @@ -27,6 +27,7 @@ import { ApiKeyEntity } from 'src/engine/core-modules/api-key/api-key.entity'; import { ApplicationService } from 'src/engine/core-modules/application/application.service'; import { ApplicationDTO } from 'src/engine/core-modules/application/dtos/application.dto'; import { fromFlatApplicationToApplicationDto } from 'src/engine/core-modules/application/utils/from-flat-application-to-application-dto.util'; +import { BillingEntitlementDTO } from 'src/engine/core-modules/billing/dtos/billing-entitlement.dto'; import { BillingSubscriptionEntity } from 'src/engine/core-modules/billing/entities/billing-subscription.entity'; import { BillingSubscriptionService } from 'src/engine/core-modules/billing/services/billing-subscription.service'; import { DomainValidRecords } from 'src/engine/core-modules/dns-manager/dtos/domain-valid-records'; @@ -320,6 +321,13 @@ export class WorkspaceResolver { return workspace.logo ?? ''; } + @ResolveField(() => [BillingEntitlementDTO]) + billingEntitlements(@Parent() workspace: WorkspaceEntity) { + return this.billingSubscriptionService.getWorkspaceEntitlements( + workspace.id, + ); + } + @ResolveField(() => Boolean) hasValidEnterpriseKey(): boolean { return isDefined(this.twentyConfigService.get('ENTERPRISE_KEY')); diff --git a/packages/twenty-server/src/engine/metadata-modules/row-level-permission-predicate/exceptions/row-level-permission-predicate-group.exception.ts b/packages/twenty-server/src/engine/metadata-modules/row-level-permission-predicate/exceptions/row-level-permission-predicate-group.exception.ts index 0219736954..3dc4636ebb 100644 --- a/packages/twenty-server/src/engine/metadata-modules/row-level-permission-predicate/exceptions/row-level-permission-predicate-group.exception.ts +++ b/packages/twenty-server/src/engine/metadata-modules/row-level-permission-predicate/exceptions/row-level-permission-predicate-group.exception.ts @@ -16,6 +16,8 @@ export const RowLevelPermissionPredicateGroupExceptionCode = 'INVALID_ROW_LEVEL_PERMISSION_PREDICATE_GROUP_DATA', ROLE_NOT_FOUND: 'ROLE_NOT_FOUND', UNAUTHORIZED_ROLE_MODIFICATION: 'UNAUTHORIZED_ROLE_MODIFICATION', + ROW_LEVEL_PERMISSION_FEATURE_DISABLED: + 'ROW_LEVEL_PERMISSION_FEATURE_DISABLED', } as const); const rowLevelPermissionPredicateGroupExceptionUserFriendlyMessages: Record< @@ -26,6 +28,7 @@ const rowLevelPermissionPredicateGroupExceptionUserFriendlyMessages: Record< INVALID_ROW_LEVEL_PERMISSION_PREDICATE_GROUP_DATA: msg`Invalid row level permission predicate group data.`, ROLE_NOT_FOUND: msg`Role not found.`, UNAUTHORIZED_ROLE_MODIFICATION: msg`Cannot modify predicate group belonging to a different role.`, + ROW_LEVEL_PERMISSION_FEATURE_DISABLED: msg`Row level permission predicate feature is disabled.`, INTERNAL_SERVER_ERROR: msg`An unexpected error occurred.`, }; diff --git a/packages/twenty-server/src/engine/metadata-modules/row-level-permission-predicate/exceptions/row-level-permission-predicate.exception.ts b/packages/twenty-server/src/engine/metadata-modules/row-level-permission-predicate/exceptions/row-level-permission-predicate.exception.ts index b21dea1f88..3875656a9d 100644 --- a/packages/twenty-server/src/engine/metadata-modules/row-level-permission-predicate/exceptions/row-level-permission-predicate.exception.ts +++ b/packages/twenty-server/src/engine/metadata-modules/row-level-permission-predicate/exceptions/row-level-permission-predicate.exception.ts @@ -19,6 +19,8 @@ export const RowLevelPermissionPredicateExceptionCode = ROLE_NOT_FOUND: 'ROLE_NOT_FOUND', UNAUTHORIZED_ROLE_MODIFICATION: 'UNAUTHORIZED_ROLE_MODIFICATION', UNAUTHORIZED_OBJECT_MODIFICATION: 'UNAUTHORIZED_OBJECT_MODIFICATION', + ROW_LEVEL_PERMISSION_FEATURE_DISABLED: + 'ROW_LEVEL_PERMISSION_FEATURE_DISABLED', } as const); const rowLevelPermissionPredicateExceptionUserFriendlyMessages: Record< @@ -32,6 +34,7 @@ const rowLevelPermissionPredicateExceptionUserFriendlyMessages: Record< ROLE_NOT_FOUND: msg`Role not found.`, UNAUTHORIZED_ROLE_MODIFICATION: msg`Cannot modify predicate belonging to a different role.`, UNAUTHORIZED_OBJECT_MODIFICATION: msg`Cannot modify predicate belonging to a different object.`, + ROW_LEVEL_PERMISSION_FEATURE_DISABLED: msg`Row level permission predicate feature is disabled.`, INTERNAL_SERVER_ERROR: msg`An unexpected error occurred.`, }; diff --git a/packages/twenty-server/src/engine/metadata-modules/row-level-permission-predicate/row-level-permission.module.ts b/packages/twenty-server/src/engine/metadata-modules/row-level-permission-predicate/row-level-permission.module.ts index fd7c8c02fe..7dfd70a66e 100644 --- a/packages/twenty-server/src/engine/metadata-modules/row-level-permission-predicate/row-level-permission.module.ts +++ b/packages/twenty-server/src/engine/metadata-modules/row-level-permission-predicate/row-level-permission.module.ts @@ -3,6 +3,7 @@ import { Module } from '@nestjs/common'; import { TypeOrmModule } from '@nestjs/typeorm'; +import { BillingModule } from 'src/engine/core-modules/billing/billing.module'; import { WorkspaceManyOrAllFlatEntityMapsCacheModule } from 'src/engine/metadata-modules/flat-entity/services/workspace-many-or-all-flat-entity-maps-cache.module'; 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'; @@ -22,6 +23,7 @@ import { WorkspaceMigrationModule } from 'src/engine/workspace-manager/workspace WorkspaceCacheModule, WorkspaceManyOrAllFlatEntityMapsCacheModule, WorkspaceMigrationModule, + BillingModule, ], providers: [ RowLevelPermissionPredicateService, 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 c533ecf66c..bb787f771b 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 @@ -1,9 +1,14 @@ /* @license Enterprise */ import { Injectable } from '@nestjs/common'; +import { ConfigService } from '@nestjs/config'; +import { InjectRepository } from '@nestjs/typeorm'; import { isDefined } from 'twenty-shared/utils'; +import { Repository } from 'typeorm'; +import { BillingEntitlementKey } from 'src/engine/core-modules/billing/enums/billing-entitlement-key.enum'; +import { BillingService } from 'src/engine/core-modules/billing/services/billing.service'; import { WorkspaceManyOrAllFlatEntityMapsCacheService } from 'src/engine/metadata-modules/flat-entity/services/workspace-many-or-all-flat-entity-maps-cache.service'; import { findFlatEntityByIdInFlatEntityMapsOrThrow } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-id-in-flat-entity-maps-or-throw.util'; import { findFlatEntityByIdInFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-id-in-flat-entity-maps.util'; @@ -17,6 +22,11 @@ import { type DeleteRowLevelPermissionPredicateGroupInput } from 'src/engine/met import { type DestroyRowLevelPermissionPredicateGroupInput } from 'src/engine/metadata-modules/row-level-permission-predicate/dtos/inputs/destroy-row-level-permission-predicate-group.input'; 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 { RowLevelPermissionPredicateGroupEntity } from 'src/engine/metadata-modules/row-level-permission-predicate/entities/row-level-permission-predicate-group.entity'; +import { + RowLevelPermissionPredicateGroupException, + RowLevelPermissionPredicateGroupExceptionCode, +} from 'src/engine/metadata-modules/row-level-permission-predicate/exceptions/row-level-permission-predicate-group.exception'; 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'; @@ -28,6 +38,10 @@ export class RowLevelPermissionPredicateGroupService { private readonly workspaceMigrationValidateBuildAndRunService: WorkspaceMigrationValidateBuildAndRunService, private readonly flatEntityMapsCacheService: WorkspaceManyOrAllFlatEntityMapsCacheService, private readonly workspaceCacheService: WorkspaceCacheService, + private readonly billingService: BillingService, + @InjectRepository(RowLevelPermissionPredicateGroupEntity) + private readonly rowLevelPermissionPredicateGroupRepository: Repository, + private readonly configService: ConfigService, ) {} async createOne({ @@ -37,6 +51,8 @@ export class RowLevelPermissionPredicateGroupService { createRowLevelPermissionPredicateGroupInput: CreateRowLevelPermissionPredicateGroupInput; workspaceId: string; }): Promise { + await this.hasRowLevelPermissionFeatureOrThrow(workspaceId); + const flatGroupToCreate = fromCreateRowLevelPermissionPredicateGroupInputToFlatRowLevelPermissionPredicateGroupToCreate( { @@ -73,6 +89,8 @@ export class RowLevelPermissionPredicateGroupService { updateRowLevelPermissionPredicateGroupInput: UpdateRowLevelPermissionPredicateGroupInput; workspaceId: string; }): Promise { + await this.hasRowLevelPermissionFeatureOrThrow(workspaceId); + const { flatRowLevelPermissionPredicateGroupMaps } = await this.flatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps( { @@ -117,6 +135,8 @@ export class RowLevelPermissionPredicateGroupService { deleteRowLevelPermissionPredicateGroupInput: DeleteRowLevelPermissionPredicateGroupInput; workspaceId: string; }): Promise { + await this.hasRowLevelPermissionFeatureOrThrow(workspaceId); + const { flatRowLevelPermissionPredicateGroupMaps } = await this.flatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps( { @@ -161,6 +181,8 @@ export class RowLevelPermissionPredicateGroupService { destroyRowLevelPermissionPredicateGroupInput: DestroyRowLevelPermissionPredicateGroupInput; workspaceId: string; }): Promise { + await this.hasRowLevelPermissionFeatureOrThrow(workspaceId); + const { flatRowLevelPermissionPredicateGroupMaps } = await this.flatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps( { @@ -188,6 +210,13 @@ export class RowLevelPermissionPredicateGroupService { async findByWorkspaceId( workspaceId: string, ): Promise { + const hasRowLevelPermissionFeature = + await this.hasRowLevelPermissionFeature(workspaceId); + + if (!hasRowLevelPermissionFeature) { + return []; + } + const { flatRowLevelPermissionPredicateGroupMaps } = await this.flatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps( { @@ -211,6 +240,13 @@ export class RowLevelPermissionPredicateGroupService { workspaceId: string, roleId: string, ): Promise { + const hasRowLevelPermissionFeature = + await this.hasRowLevelPermissionFeature(workspaceId); + + if (!hasRowLevelPermissionFeature) { + return []; + } + const { flatRowLevelPermissionPredicateGroupMaps } = await this.flatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps( { @@ -234,6 +270,13 @@ export class RowLevelPermissionPredicateGroupService { id: string, workspaceId: string, ): Promise { + const hasRowLevelPermissionFeature = + await this.hasRowLevelPermissionFeature(workspaceId); + + if (!hasRowLevelPermissionFeature) { + return null; + } + const { flatRowLevelPermissionPredicateGroupMaps } = await this.flatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps( { @@ -254,6 +297,18 @@ export class RowLevelPermissionPredicateGroupService { return fromFlatRowLevelPermissionPredicateGroupToDto(flatGroup); } + public async deleteAllRowLevelPermissionPredicateGroups(workspaceId: string) { + await this.rowLevelPermissionPredicateGroupRepository.delete({ + workspaceId, + }); + + await this.workspaceCacheService.invalidateAndRecompute(workspaceId, [ + 'rolesPermissions', + 'flatRowLevelPermissionPredicateMaps', + 'flatRowLevelPermissionPredicateGroupMaps', + ]); + } + private async runMigration({ workspaceId, flatEntityToCreate = [], @@ -292,4 +347,35 @@ export class RowLevelPermissionPredicateGroupService { 'flatRowLevelPermissionPredicateMaps', ]); } + + private async hasRowLevelPermissionFeature( + workspaceId: string, + ): Promise { + const isBillingEnabled = this.configService.get('IS_BILLING_ENABLED'); + const entrepriseKey = this.configService.get('ENTERPRISE_KEY'); + + const isRowLevelPermissionEnabled = + await this.billingService.hasEntitlement( + workspaceId, + BillingEntitlementKey.RLS, + ); + + if (isDefined(entrepriseKey)) { + return true; + } + + return isBillingEnabled && isRowLevelPermissionEnabled; + } + + private async hasRowLevelPermissionFeatureOrThrow(workspaceId: string) { + const hasRowLevelPermissionFeature = + await this.hasRowLevelPermissionFeature(workspaceId); + + if (!hasRowLevelPermissionFeature) { + throw new RowLevelPermissionPredicateGroupException( + 'Row level permission predicate feature is disabled', + RowLevelPermissionPredicateGroupExceptionCode.ROW_LEVEL_PERMISSION_FEATURE_DISABLED, + ); + } + } } 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 e5c8cb2fba..66a2b3182d 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 @@ -1,10 +1,13 @@ /* @license Enterprise */ import { Injectable } from '@nestjs/common'; +import { ConfigService } from '@nestjs/config'; import { isDefined } from 'twenty-shared/utils'; import { v4 } from 'uuid'; +import { BillingEntitlementKey } from 'src/engine/core-modules/billing/enums/billing-entitlement-key.enum'; +import { BillingService } from 'src/engine/core-modules/billing/services/billing.service'; import { WorkspaceManyOrAllFlatEntityMapsCacheService } from 'src/engine/metadata-modules/flat-entity/services/workspace-many-or-all-flat-entity-maps-cache.service'; import { type FlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-maps.type'; import { findFlatEntityByIdInFlatEntityMapsOrThrow } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-id-in-flat-entity-maps-or-throw.util'; @@ -26,6 +29,10 @@ import { } from 'src/engine/metadata-modules/row-level-permission-predicate/dtos/inputs/upsert-row-level-permission-predicates.input'; 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'; +import { + RowLevelPermissionPredicateException, + RowLevelPermissionPredicateExceptionCode, +} from 'src/engine/metadata-modules/row-level-permission-predicate/exceptions/row-level-permission-predicate.exception'; 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'; @@ -38,6 +45,8 @@ export class RowLevelPermissionPredicateService { private readonly workspaceMigrationValidateBuildAndRunService: WorkspaceMigrationValidateBuildAndRunService, private readonly flatEntityMapsCacheService: WorkspaceManyOrAllFlatEntityMapsCacheService, private readonly workspaceCacheService: WorkspaceCacheService, + private readonly billingService: BillingService, + private readonly configService: ConfigService, ) {} async createOne({ @@ -47,6 +56,8 @@ export class RowLevelPermissionPredicateService { createRowLevelPermissionPredicateInput: CreateRowLevelPermissionPredicateInput; workspaceId: string; }): Promise { + await this.hasRowLevelPermissionFeatureOrThrow(workspaceId); + const flatPredicateToCreate = fromCreateRowLevelPermissionPredicateInputToFlatRowLevelPermissionPredicateToCreate( { @@ -83,6 +94,8 @@ export class RowLevelPermissionPredicateService { updateRowLevelPermissionPredicateInput: UpdateRowLevelPermissionPredicateInput; workspaceId: string; }): Promise { + await this.hasRowLevelPermissionFeatureOrThrow(workspaceId); + const { flatRowLevelPermissionPredicateMaps } = await this.flatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps( { @@ -127,6 +140,8 @@ export class RowLevelPermissionPredicateService { deleteRowLevelPermissionPredicateInput: DeleteRowLevelPermissionPredicateInput; workspaceId: string; }): Promise { + await this.hasRowLevelPermissionFeatureOrThrow(workspaceId); + const { flatRowLevelPermissionPredicateMaps } = await this.flatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps( { @@ -171,6 +186,8 @@ export class RowLevelPermissionPredicateService { destroyRowLevelPermissionPredicateInput: DestroyRowLevelPermissionPredicateInput; workspaceId: string; }): Promise { + await this.hasRowLevelPermissionFeatureOrThrow(workspaceId); + const { flatRowLevelPermissionPredicateMaps } = await this.flatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps( { @@ -198,6 +215,13 @@ export class RowLevelPermissionPredicateService { async findByWorkspaceId( workspaceId: string, ): Promise { + const hasRowLevelPermissionFeature = + await this.hasRowLevelPermissionFeature(workspaceId); + + if (!hasRowLevelPermissionFeature) { + return []; + } + const { flatRowLevelPermissionPredicateMaps } = await this.flatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps( { @@ -222,6 +246,13 @@ export class RowLevelPermissionPredicateService { roleId: string, objectMetadataId: string, ): Promise { + const hasRowLevelPermissionFeature = + await this.hasRowLevelPermissionFeature(workspaceId); + + if (!hasRowLevelPermissionFeature) { + return []; + } + const { flatRowLevelPermissionPredicateMaps } = await this.flatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps( { @@ -250,6 +281,13 @@ export class RowLevelPermissionPredicateService { id: string, workspaceId: string, ): Promise { + const hasRowLevelPermissionFeature = + await this.hasRowLevelPermissionFeature(workspaceId); + + if (!hasRowLevelPermissionFeature) { + return null; + } + const { flatRowLevelPermissionPredicateMaps } = await this.flatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps( { @@ -280,6 +318,8 @@ export class RowLevelPermissionPredicateService { predicates: RowLevelPermissionPredicateDTO[]; predicateGroups: RowLevelPermissionPredicateGroupDTO[]; }> { + await this.hasRowLevelPermissionFeatureOrThrow(workspaceId); + const { roleId, objectMetadataId, predicates, predicateGroups } = input; const { @@ -653,4 +693,35 @@ export class RowLevelPermissionPredicateService { 'flatRowLevelPermissionPredicateGroupMaps', ]); } + + private async hasRowLevelPermissionFeature( + workspaceId: string, + ): Promise { + const isBillingEnabled = this.configService.get('IS_BILLING_ENABLED'); + const entrepriseKey = this.configService.get('ENTERPRISE_KEY'); + + const isRowLevelPermissionEnabled = + await this.billingService.hasEntitlement( + workspaceId, + BillingEntitlementKey.RLS, + ); + + if (isDefined(entrepriseKey)) { + return true; + } + + return isBillingEnabled && isRowLevelPermissionEnabled; + } + + private async hasRowLevelPermissionFeatureOrThrow(workspaceId: string) { + const hasRowLevelPermissionFeature = + await this.hasRowLevelPermissionFeature(workspaceId); + + if (!hasRowLevelPermissionFeature) { + throw new RowLevelPermissionPredicateException( + 'Row level permission predicate feature is disabled', + RowLevelPermissionPredicateExceptionCode.ROW_LEVEL_PERMISSION_FEATURE_DISABLED, + ); + } + } } 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 index b607490b0f..23647c00c4 100644 --- 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 @@ -13,6 +13,7 @@ import { type EmailsFilter, type FloatFilter, type FullNameFilter, + type IsFilter, type LeafObjectRecordFilter, type LinksFilter, type MultiSelectFilter, @@ -210,6 +211,10 @@ export const isRecordMatchingRLSRowLevelPermissionPredicate = ({ const recordFieldValue = record[filterKey]; if (!isDefined(recordFieldValue)) { + if (isObject(filterValue)) { + return (filterValue as { is?: IsFilter })?.is === 'NULL'; + } + return false; }