[AI] Remove deep-equal package (#7187)

* Remove `deep-equal` package

* Add release notes

* Add a few more tests

* Add release notes
This commit is contained in:
Julian Dominguez-Schatz
2026-03-13 12:36:52 -04:00
committed by GitHub
parent 5d4bbc9ebb
commit 85e3166495
5 changed files with 988 additions and 143 deletions

View File

@@ -75,7 +75,6 @@
"csv-parse": "^6.1.0",
"csv-stringify": "^6.6.0",
"date-fns": "^4.1.0",
"deep-equal": "^2.2.3",
"handlebars": "^4.7.8",
"lru-cache": "^11.2.5",
"md5": "^2.3.0",

View File

@@ -8,6 +8,7 @@ import { loadMappings } from '../db/mappings';
import { loadRules, updateRule } from '../transactions/transaction-rules';
import {
areConditionValuesEqual,
createSchedule,
deleteSchedule,
setNextDate,
@@ -79,6 +80,49 @@ describe('schedule app', () => {
}),
).toBe('2020-12-30');
});
it('areConditionValuesEqual matches nested objects regardless of key order', () => {
expect(
areConditionValuesEqual(
{
value: {
start: '2020-12-20',
frequency: 'monthly',
patterns: [
{ type: 'day', value: 15 },
{ type: 'day', value: 30 },
],
},
field: 'date',
},
{
field: 'date',
value: {
patterns: [
{ value: 15, type: 'day' },
{ value: 30, type: 'day' },
],
frequency: 'monthly',
start: '2020-12-20',
},
},
),
).toBe(true);
});
it('areConditionValuesEqual returns false for different array ordering', () => {
expect(
areConditionValuesEqual(
[{ field: 'date' }, { field: 'account' }],
[{ field: 'account' }, { field: 'date' }],
),
).toBe(false);
});
it('areConditionValuesEqual distinguishes nullish values', () => {
expect(areConditionValuesEqual(null, undefined)).toBe(false);
expect(areConditionValuesEqual(undefined, undefined)).toBe(true);
});
});
describe('methods', () => {
@@ -176,6 +220,85 @@ describe('schedule app', () => {
expect(row.posts_transaction).toBe(true);
});
it('updateSchedule does not update `next_date` when unrelated conditions change', async () => {
const id = await createSchedule({
conditions: [
{ op: 'is', field: 'payee', value: 'foo' },
{
op: 'is',
field: 'date',
value: {
start: '2020-12-20',
frequency: 'monthly',
patterns: [
{ type: 'day', value: 15 },
{ type: 'day', value: 30 },
],
},
},
],
});
MockDate.set(new Date(2021, 4, 17));
await updateSchedule({
schedule: { id },
conditions: [{ op: 'is', field: 'payee', value: 'bar' }],
});
const {
data: [row],
} = await aqlQuery(q('schedules').filter({ id }).select(['next_date']));
expect(row.next_date).toBe('2020-12-30');
});
it('updateSchedule ignores the condition `type` field when date value is unchanged', async () => {
const id = await createSchedule({
conditions: [
{
op: 'is',
field: 'date',
value: {
start: '2020-12-20',
frequency: 'monthly',
patterns: [
{ type: 'day', value: 15 },
{ type: 'day', value: 30 },
],
},
},
],
});
MockDate.set(new Date(2021, 4, 17));
await updateSchedule({
schedule: { id },
conditions: [
{
op: 'is',
field: 'date',
type: 'date',
value: {
start: '2020-12-20',
frequency: 'monthly',
patterns: [
{ type: 'day', value: 15 },
{ type: 'day', value: 30 },
],
},
},
],
});
const {
data: [row],
} = await aqlQuery(q('schedules').filter({ id }).select(['next_date']));
expect(row.next_date).toBe('2020-12-30');
});
it('deleteSchedule deletes a schedule', async () => {
const id = await createSchedule({
conditions: [

View File

@@ -1,6 +1,5 @@
// @ts-strict-ignore
import * as d from 'date-fns';
import deepEqual from 'deep-equal';
import { v4 as uuidv4 } from 'uuid';
import { captureBreadcrumb } from '../../platform/exceptions';
@@ -17,7 +16,7 @@ import {
getStatus,
recurConfigToRSchedule,
} from '../../shared/schedules';
import type { ScheduleEntity } from '../../types/models';
import type { RuleConditionEntity, ScheduleEntity } from '../../types/models';
import { addTransactions } from '../accounts/sync';
import { createApp } from '../app';
import { aqlQuery } from '../aql';
@@ -48,6 +47,57 @@ function zip(arr1, arr2) {
return result;
}
export function areConditionValuesEqual(left, right) {
if (left === right) {
return true;
}
if (left == null || right == null) {
return left === right;
}
if (Array.isArray(left) || Array.isArray(right)) {
return (
Array.isArray(left) &&
Array.isArray(right) &&
left.length === right.length &&
left.every((value, index) => areConditionValuesEqual(value, right[index]))
);
}
if (typeof left === 'object' && typeof right === 'object') {
const leftKeys = Object.keys(left).sort();
const rightKeys = Object.keys(right).sort();
return (
leftKeys.length === rightKeys.length &&
leftKeys.every((key, index) => {
const rightKey = rightKeys[index];
return (
key === rightKey &&
areConditionValuesEqual(left[key], right[rightKey])
);
})
);
}
return false;
}
function areScheduleConditionsEqual(
left?: RuleConditionEntity,
right?: RuleConditionEntity,
) {
if (left == null || right == null) {
return left === right;
}
const { type: _leftType, ...leftCondition } = left;
const { type: _rightType, ...rightCondition } = right;
return areConditionValuesEqual(leftCondition, rightCondition);
}
export function updateConditions(conditions, newConditions) {
const scheduleConds = extractScheduleConds(conditions);
const newScheduleConds = extractScheduleConds(newConditions);
@@ -260,8 +310,8 @@ export async function updateSchedule({
conditions,
resetNextDate,
}: {
schedule;
conditions?;
schedule: Partial<ScheduleEntity> & Pick<ScheduleEntity, 'id'>;
conditions?: RuleConditionEntity[];
resetNextDate?: boolean;
}) {
if (schedule.rule) {
@@ -306,11 +356,11 @@ export async function updateSchedule({
// might switch accounts from a closed one
if (
resetNextDate ||
!deepEqual(
oldConditions.find(c => c.field === 'account'),
!areScheduleConditionsEqual(
oldConditions.find(c => c.field === 'account'),
newConditions.find(c => c.field === 'account'),
) ||
!deepEqual(
!areConditionValuesEqual(
stripType(oldConditions.find(c => c.field === 'date') || {}),
stripType(newConditions.find(c => c.field === 'date') || {}),
)