Automatically clean up soft-deleted records after X days. (#14862)
Closes #14726 ### Added - `trashRetentionDays` field to workspace entity (default: 14 days) - Automated trash cleanup using BullMQ jobs - Daily cron (00:10 UTC) that enqueues cleanup jobs for all active workspaces - Per-workspace limit: 100k records deleted per day - Calendar-based retention: records deleted on day X are cleaned up X+14 days later (at midnight UTC boundaries) ### Architecture - **Cron (WorkspaceTrashCleanupCronJob):** Runs daily, enqueues jobs in parallel for all workspaces - **Job (WorkspaceTrashCleanupJob):** Processes individual workspace cleanup - **Service (WorkspaceTrashCleanupService):** Discovers tables with `deletedAt`, deletes old records with quota enforcement - **Command:** `npx nx run twenty-server:command cron:workspace:cleanup-trash` to register the cron ### Testing - Unit tests for service with 100% coverage of public API - Tested quota enforcement, error handling, and edge cases --------- Co-authored-by: Félix Malfait <felix.malfait@gmail.com>
This commit is contained in:
@@ -10,14 +10,16 @@ type SettingsCounterProps = {
|
||||
minValue?: number;
|
||||
maxValue?: number;
|
||||
disabled?: boolean;
|
||||
showButtons?: boolean;
|
||||
};
|
||||
|
||||
const StyledCounterContainer = styled.div`
|
||||
const StyledCounterContainer = styled.div<{ showButtons: boolean }>`
|
||||
align-items: center;
|
||||
display: flex;
|
||||
gap: ${({ theme }) => theme.spacing(1)};
|
||||
margin-left: auto;
|
||||
width: ${({ theme }) => theme.spacing(30)};
|
||||
width: ${({ theme, showButtons }) =>
|
||||
showButtons ? theme.spacing(30) : theme.spacing(16)};
|
||||
`;
|
||||
|
||||
const StyledTextInput = styled(SettingsTextInput)`
|
||||
@@ -34,11 +36,12 @@ export const SettingsCounter = ({
|
||||
value,
|
||||
onChange,
|
||||
minValue = 0,
|
||||
maxValue = 100,
|
||||
maxValue,
|
||||
disabled = false,
|
||||
showButtons = true,
|
||||
}: SettingsCounterProps) => {
|
||||
const handleIncrementCounter = () => {
|
||||
if (value < maxValue) {
|
||||
if (maxValue === undefined || value < maxValue) {
|
||||
onChange(value + 1);
|
||||
}
|
||||
};
|
||||
@@ -60,7 +63,7 @@ export const SettingsCounter = ({
|
||||
return;
|
||||
}
|
||||
|
||||
if (castedNumber > maxValue) {
|
||||
if (maxValue !== undefined && castedNumber > maxValue) {
|
||||
onChange(maxValue);
|
||||
return;
|
||||
}
|
||||
@@ -68,14 +71,16 @@ export const SettingsCounter = ({
|
||||
};
|
||||
|
||||
return (
|
||||
<StyledCounterContainer>
|
||||
<IconButton
|
||||
size="small"
|
||||
Icon={IconMinus}
|
||||
variant="secondary"
|
||||
onClick={handleDecrementCounter}
|
||||
disabled={disabled}
|
||||
/>
|
||||
<StyledCounterContainer showButtons={showButtons}>
|
||||
{showButtons && (
|
||||
<IconButton
|
||||
size="small"
|
||||
Icon={IconMinus}
|
||||
variant="secondary"
|
||||
onClick={handleDecrementCounter}
|
||||
disabled={disabled}
|
||||
/>
|
||||
)}
|
||||
<StyledTextInput
|
||||
instanceId="settings-counter-input"
|
||||
name="counter"
|
||||
@@ -84,13 +89,15 @@ export const SettingsCounter = ({
|
||||
onChange={handleTextInputChange}
|
||||
disabled={disabled}
|
||||
/>
|
||||
<IconButton
|
||||
size="small"
|
||||
Icon={IconPlus}
|
||||
variant="secondary"
|
||||
onClick={handleIncrementCounter}
|
||||
disabled={disabled}
|
||||
/>
|
||||
{showButtons && (
|
||||
<IconButton
|
||||
size="small"
|
||||
Icon={IconPlus}
|
||||
variant="secondary"
|
||||
onClick={handleIncrementCounter}
|
||||
disabled={disabled}
|
||||
/>
|
||||
)}
|
||||
</StyledCounterContainer>
|
||||
);
|
||||
};
|
||||
|
||||
+3
@@ -17,6 +17,7 @@ type SettingsOptionCardContentCounterProps = {
|
||||
onChange: (value: number) => void;
|
||||
minValue?: number;
|
||||
maxValue?: number;
|
||||
showButtons?: boolean;
|
||||
};
|
||||
|
||||
export const SettingsOptionCardContentCounter = ({
|
||||
@@ -28,6 +29,7 @@ export const SettingsOptionCardContentCounter = ({
|
||||
onChange,
|
||||
minValue,
|
||||
maxValue,
|
||||
showButtons = true,
|
||||
}: SettingsOptionCardContentCounterProps) => {
|
||||
return (
|
||||
<StyledSettingsOptionCardContent disabled={disabled}>
|
||||
@@ -50,6 +52,7 @@ export const SettingsOptionCardContentCounter = ({
|
||||
minValue={minValue}
|
||||
maxValue={maxValue}
|
||||
disabled={disabled}
|
||||
showButtons={showButtons}
|
||||
/>
|
||||
</StyledSettingsOptionCardContent>
|
||||
);
|
||||
|
||||
+15
@@ -25,6 +25,7 @@ const SettingsOptionCardContentCounterWrapper = (
|
||||
disabled={args.disabled}
|
||||
minValue={args.minValue}
|
||||
maxValue={args.maxValue}
|
||||
showButtons={args.showButtons}
|
||||
/>
|
||||
</StyledContainer>
|
||||
);
|
||||
@@ -50,6 +51,7 @@ export const Default: Story = {
|
||||
value: 5,
|
||||
minValue: 1,
|
||||
maxValue: 10,
|
||||
showButtons: true,
|
||||
},
|
||||
argTypes: {
|
||||
Icon: { control: false },
|
||||
@@ -64,6 +66,7 @@ export const WithoutIcon: Story = {
|
||||
value: 20,
|
||||
minValue: 10,
|
||||
maxValue: 50,
|
||||
showButtons: true,
|
||||
},
|
||||
};
|
||||
|
||||
@@ -76,5 +79,17 @@ export const Disabled: Story = {
|
||||
disabled: true,
|
||||
minValue: 1,
|
||||
maxValue: 10,
|
||||
showButtons: true,
|
||||
},
|
||||
};
|
||||
|
||||
export const WithoutButtons: Story = {
|
||||
args: {
|
||||
Icon: IconUsers,
|
||||
title: 'Trash Retention',
|
||||
description: 'Adjust the number of days before deletion',
|
||||
value: 14,
|
||||
minValue: 0,
|
||||
showButtons: false,
|
||||
},
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user