Merge pull request #263 from 5h0ov/new-olderthan-segmentfilter
This commit is contained in:
@@ -584,8 +584,10 @@ export class SegmentService {
|
||||
'exists',
|
||||
'notExists',
|
||||
'within',
|
||||
'olderThan',
|
||||
'triggered',
|
||||
'triggeredWithin',
|
||||
'triggeredOlderThan',
|
||||
'notTriggered',
|
||||
];
|
||||
|
||||
@@ -604,7 +606,9 @@ export class SegmentService {
|
||||
'greaterThanOrEqual',
|
||||
'lessThanOrEqual',
|
||||
'within',
|
||||
'olderThan',
|
||||
'triggeredWithin',
|
||||
'triggeredOlderThan',
|
||||
];
|
||||
|
||||
if (operatorsNeedingValue.includes(filter.operator) && filter.value === undefined) {
|
||||
@@ -612,7 +616,7 @@ export class SegmentService {
|
||||
}
|
||||
|
||||
// Validate unit for time-based operators
|
||||
if (['within', 'triggeredWithin'].includes(filter.operator) && !filter.unit) {
|
||||
if (['within', 'triggeredWithin', 'olderThan', 'triggeredOlderThan'].includes(filter.operator) && !filter.unit) {
|
||||
throw new HttpException(400, `"${filter.operator}" operator requires a unit (days, hours, or minutes)`);
|
||||
}
|
||||
}
|
||||
@@ -729,6 +733,20 @@ export class SegmentService {
|
||||
// Use ISO string for lexicographic comparison in JSON
|
||||
return {data: {path, gte: since.toISOString() as Prisma.InputJsonValue}};
|
||||
}
|
||||
case 'olderThan': {
|
||||
// Note: Requires JSON date fields in ISO 8601 format for proper comparison
|
||||
if (!unit) {
|
||||
throw new HttpException(400, 'Unit is required for "olderThan" operator');
|
||||
}
|
||||
|
||||
// Calculate the "before" date (X time units ago from now)
|
||||
const now = new Date();
|
||||
const milliseconds = this.getMilliseconds(value as number, unit);
|
||||
const before = new Date(now.getTime() - milliseconds);
|
||||
|
||||
// Use ISO string for lexicographic comparison in JSON
|
||||
return {data: {path, lt: before.toISOString() as Prisma.InputJsonValue}};
|
||||
}
|
||||
default:
|
||||
throw new HttpException(400, `Unsupported operator for JSON field: ${operator}`);
|
||||
}
|
||||
@@ -822,6 +840,18 @@ export class SegmentService {
|
||||
|
||||
return {[field]: {gte: since}};
|
||||
}
|
||||
case 'olderThan': {
|
||||
// "olderThan X days/hours/minutes" means more than X time units ago
|
||||
if (!unit) {
|
||||
throw new HttpException(400, 'Unit is required for "olderThan" operator');
|
||||
}
|
||||
|
||||
const now = new Date();
|
||||
const milliseconds = this.getMilliseconds(value as number, unit);
|
||||
const before = new Date(now.getTime() - milliseconds);
|
||||
|
||||
return {[field]: {lt: before}};
|
||||
}
|
||||
default:
|
||||
throw new HttpException(400, `Unsupported operator for date field: ${operator}`);
|
||||
}
|
||||
@@ -921,6 +951,42 @@ export class SegmentService {
|
||||
};
|
||||
}
|
||||
|
||||
case 'triggeredOlderThan': {
|
||||
// Contact triggered this event, but only more than X time ago (not recently)
|
||||
// This means: has event AND all occurrences are before the cutoff
|
||||
if (!unit) {
|
||||
throw new HttpException(400, 'Unit is required for "triggeredOlderThan" operator');
|
||||
}
|
||||
|
||||
const now = new Date();
|
||||
const milliseconds = this.getMilliseconds(value as number, unit);
|
||||
const before = new Date(now.getTime() - milliseconds);
|
||||
|
||||
return {
|
||||
AND: [
|
||||
// Must have triggered the event at some point
|
||||
{
|
||||
events: {
|
||||
some: {
|
||||
name: eventName,
|
||||
},
|
||||
},
|
||||
},
|
||||
// But NOT within the recent timeframe
|
||||
{
|
||||
events: {
|
||||
none: {
|
||||
name: eventName,
|
||||
createdAt: {
|
||||
gte: before,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
case 'notTriggered':
|
||||
// Contact has never triggered this event
|
||||
return {
|
||||
@@ -995,6 +1061,42 @@ export class SegmentService {
|
||||
};
|
||||
}
|
||||
|
||||
case 'triggeredOlderThan': {
|
||||
// Contact had this email activity, but only more than X time ago (not recently)
|
||||
if (!unit) {
|
||||
throw new HttpException(400, 'Unit is required for "triggeredOlderThan" operator');
|
||||
}
|
||||
|
||||
const now = new Date();
|
||||
const milliseconds = this.getMilliseconds(value as number, unit);
|
||||
const before = new Date(now.getTime() - milliseconds);
|
||||
|
||||
return {
|
||||
AND: [
|
||||
// Must have the email activity at some point
|
||||
{
|
||||
emails: {
|
||||
some: {
|
||||
[field]: {
|
||||
not: null,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
// But NOT within the recent timeframe
|
||||
{
|
||||
emails: {
|
||||
none: {
|
||||
[field]: {
|
||||
gte: before,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
case 'notTriggered':
|
||||
// Contact has never had this email activity
|
||||
return {
|
||||
|
||||
@@ -27,12 +27,14 @@ const STANDARD_OPERATORS: {value: SegmentFilterOperator; label: string}[] = [
|
||||
{value: 'lessThanOrEqual', label: 'Less than or equal to'},
|
||||
{value: 'exists', label: 'Exists'},
|
||||
{value: 'notExists', label: 'Does not exist'},
|
||||
{value: 'within', label: 'Within (time)'},
|
||||
{value: 'within', label: 'Younger than (time)'},
|
||||
{value: 'olderThan', label: 'Older than (time)'},
|
||||
];
|
||||
|
||||
const EVENT_OPERATORS: {value: SegmentFilterOperator; label: string}[] = [
|
||||
{value: 'triggered', label: 'Ever occurred'},
|
||||
{value: 'triggeredWithin', label: 'Occurred within'},
|
||||
{value: 'triggeredOlderThan', label: 'Occurred over (time) ago'},
|
||||
{value: 'notTriggered', label: 'Never occurred'},
|
||||
];
|
||||
|
||||
@@ -172,6 +174,7 @@ const FilterRow = memo(function FilterRow({filter, onChange, onRemove, available
|
||||
'exists',
|
||||
'notExists',
|
||||
'within',
|
||||
'olderThan',
|
||||
].includes(op.value),
|
||||
);
|
||||
}
|
||||
@@ -183,7 +186,7 @@ const FilterRow = memo(function FilterRow({filter, onChange, onRemove, available
|
||||
}, []);
|
||||
|
||||
const needsValue = !['exists', 'notExists', 'triggered', 'notTriggered'].includes(filter.operator);
|
||||
const needsUnit = ['within', 'triggeredWithin'].includes(filter.operator);
|
||||
const needsUnit = ['within', 'triggeredWithin', 'olderThan', 'triggeredOlderThan'].includes(filter.operator);
|
||||
|
||||
// Get field type from available fields
|
||||
const fieldOption = useMemo(
|
||||
@@ -217,6 +220,7 @@ const FilterRow = memo(function FilterRow({filter, onChange, onRemove, available
|
||||
'exists',
|
||||
'notExists',
|
||||
'within',
|
||||
'olderThan',
|
||||
].includes(op.value),
|
||||
);
|
||||
}
|
||||
@@ -232,7 +236,9 @@ const FilterRow = memo(function FilterRow({filter, onChange, onRemove, available
|
||||
const selectedField = availableFields.find(f => f.value === value);
|
||||
const newFieldType = selectedField?.type || 'string';
|
||||
const isEvent = newFieldType === 'event' || newFieldType === 'email';
|
||||
const currentOperatorIsEvent = ['triggered', 'triggeredWithin', 'notTriggered'].includes(filter.operator);
|
||||
const currentOperatorIsEvent = ['triggered', 'triggeredWithin', 'triggeredOlderThan', 'notTriggered'].includes(
|
||||
filter.operator,
|
||||
);
|
||||
|
||||
// Determine default operator and value based on new field type
|
||||
let newOperator = filter.operator;
|
||||
@@ -267,7 +273,9 @@ const FilterRow = memo(function FilterRow({filter, onChange, onRemove, available
|
||||
newUnit = undefined;
|
||||
|
||||
// If the new operator doesn't support units but we had them, ensure value is appropriate
|
||||
const newOperatorNeedsUnit = ['within', 'triggeredWithin'].includes(newOperator);
|
||||
const newOperatorNeedsUnit = ['within', 'triggeredWithin', 'olderThan', 'triggeredOlderThan'].includes(
|
||||
newOperator,
|
||||
);
|
||||
if (!newOperatorNeedsUnit) {
|
||||
// Convert numeric value back to appropriate type for the field
|
||||
newValue = getDefaultValueForType(newFieldType);
|
||||
@@ -399,8 +407,12 @@ const FilterRow = memo(function FilterRow({filter, onChange, onRemove, available
|
||||
// Check if we're switching between operators that need different value types
|
||||
const oldNeedsValue = !['exists', 'notExists', 'triggered', 'notTriggered'].includes(oldOperator);
|
||||
const newNeedsValue = !['exists', 'notExists', 'triggered', 'notTriggered'].includes(newOperator);
|
||||
const oldNeedsUnit = ['within', 'triggeredWithin'].includes(oldOperator);
|
||||
const newNeedsUnit = ['within', 'triggeredWithin'].includes(newOperator);
|
||||
const oldNeedsUnit = ['within', 'triggeredWithin', 'olderThan', 'triggeredOlderThan'].includes(
|
||||
oldOperator,
|
||||
);
|
||||
const newNeedsUnit = ['within', 'triggeredWithin', 'olderThan', 'triggeredOlderThan'].includes(
|
||||
newOperator,
|
||||
);
|
||||
|
||||
const updatedFilter: SegmentFilter = {...filter, operator: newOperator};
|
||||
|
||||
|
||||
@@ -101,8 +101,10 @@ const segmentFilterSchema = z.object({
|
||||
'exists',
|
||||
'notExists',
|
||||
'within',
|
||||
'olderThan',
|
||||
'triggered',
|
||||
'triggeredWithin',
|
||||
'triggeredOlderThan',
|
||||
'notTriggered',
|
||||
]),
|
||||
value: z.any().optional(),
|
||||
|
||||
@@ -16,9 +16,11 @@ export type SegmentFilterOperator =
|
||||
| 'exists'
|
||||
| 'notExists'
|
||||
| 'within'
|
||||
| 'olderThan' // More than X time ago (for date fields)
|
||||
// Event-based operators
|
||||
| 'triggered' // Event/email activity occurred (any time)
|
||||
| 'triggeredWithin' // Event/email activity occurred within timeframe
|
||||
| 'triggeredOlderThan' // Event/email activity occurred more than X time ago
|
||||
| 'notTriggered'; // Event/email activity never occurred
|
||||
|
||||
export type SegmentFilterLogic = 'AND' | 'OR';
|
||||
|
||||
Reference in New Issue
Block a user